@arronqzy/react-view 1.0.3 → 1.0.5
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/dist/styles.css +56 -26
- package/package.json +6 -7
- package/readme.md +74 -5
- package/src/panel/ReactViewPanel.tsx +17 -7
- package/src/panel/components/ConfigHintIcon.tsx +46 -0
- package/src/panel/components/ElementsLayer.tsx +2 -4
- package/src/panel/components/MoveableLayer.tsx +68 -55
- package/src/panel/components/PanelCanvas.tsx +81 -64
- package/src/panel/components/PanelConfigSidebar.tsx +55 -77
- package/src/panel/components/SelectLayer.tsx +1 -0
- package/src/panel/components/ViewElementScopePanel.tsx +5 -8
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +238 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +24 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +1 -1
- package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
- package/src/panel/types.ts +2 -0
- package/src/panel/utils/chartOptionBuilder.ts +12 -1
- package/src/panel/utils/scope-field-labels.ts +1 -0
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +124 -0
- package/src/panel/utils/scope-template-spread.ts +229 -0
- package/src/panel/utils/scope-template-warnings.ts +110 -2
- package/src/panel/utils/scope-template.ts +25 -3
- package/src/tailwind.css +27 -14
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import InfiniteViewer from "react-infinite-viewer";
|
|
3
|
-
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
|
4
3
|
|
|
5
4
|
import {
|
|
6
5
|
clampViewportZoom,
|
|
@@ -23,6 +22,8 @@ export type PanelCanvasProps = {
|
|
|
23
22
|
onDropMaterial?: (payload: { materialId: string; x: number; y: number }) => void;
|
|
24
23
|
/** 渲染在 InfiniteViewer 内、画布 scale 层之外(如 Moveable 控制框) */
|
|
25
24
|
viewportOverlay?: React.ReactNode;
|
|
25
|
+
/** 平移等视口变换后同步 Moveable 控制框(绕过 React scroll 节流) */
|
|
26
|
+
viewportSyncRef?: React.MutableRefObject<(() => void) | null>;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
@@ -40,12 +41,12 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
40
41
|
onCanvasContextMenuCapture,
|
|
41
42
|
onDropMaterial,
|
|
42
43
|
viewportOverlay,
|
|
44
|
+
viewportSyncRef,
|
|
43
45
|
},
|
|
44
46
|
scrollRef
|
|
45
47
|
) => {
|
|
46
48
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
|
47
49
|
const viewerRef = useRef<any>(null);
|
|
48
|
-
const transformRef = useRef<any>(null);
|
|
49
50
|
const canvasElRef = useRef<HTMLDivElement | null>(null);
|
|
50
51
|
const syncingZoomRef = useRef(false);
|
|
51
52
|
const didAutoCenterRef = useRef(false);
|
|
@@ -76,6 +77,14 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
76
77
|
height: number;
|
|
77
78
|
} | null>(null);
|
|
78
79
|
|
|
80
|
+
const applyCanvasTransform = useCallback((scale: number) => {
|
|
81
|
+
const el = canvasElRef.current;
|
|
82
|
+
if (!el) return;
|
|
83
|
+
el.style.transform = `scale(${scale})`;
|
|
84
|
+
el.style.transformOrigin = "0 0";
|
|
85
|
+
el.dataset.canvasScale = String(scale);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
79
88
|
const commitScroll = useCallback(
|
|
80
89
|
(left: number, top: number) => {
|
|
81
90
|
const next = { left, top };
|
|
@@ -85,6 +94,10 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
85
94
|
[onScrollChange]
|
|
86
95
|
);
|
|
87
96
|
|
|
97
|
+
const notifyViewportSync = useCallback(() => {
|
|
98
|
+
viewportSyncRef?.current?.();
|
|
99
|
+
}, [viewportSyncRef]);
|
|
100
|
+
|
|
88
101
|
useEffect(() => {
|
|
89
102
|
zoomRef.current = zoom;
|
|
90
103
|
}, [zoom]);
|
|
@@ -155,8 +168,6 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
155
168
|
if (!el) return;
|
|
156
169
|
|
|
157
170
|
const onWheelZoom = (e: WheelEvent) => {
|
|
158
|
-
const api = transformRef.current;
|
|
159
|
-
if (!api) return;
|
|
160
171
|
const viewer = viewerRef.current as any;
|
|
161
172
|
if (!viewer) return;
|
|
162
173
|
const target = e.target as HTMLElement | null;
|
|
@@ -189,10 +200,12 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
189
200
|
const worldY = (pointerY + currentScrollTop) / Math.max(0.0001, zy);
|
|
190
201
|
const nextScrollLeft = worldX * nextZoom.x - pointerX;
|
|
191
202
|
const nextScrollTop = worldY * nextZoom.y - pointerY;
|
|
203
|
+
const nextScale = uniformViewportZoom(nextZoom);
|
|
192
204
|
|
|
193
205
|
syncingZoomRef.current = true;
|
|
206
|
+
zoomRef.current = nextZoom;
|
|
207
|
+
applyCanvasTransform(nextScale);
|
|
194
208
|
viewer.scrollTo(nextScrollLeft, nextScrollTop);
|
|
195
|
-
commitScroll(nextScrollLeft, nextScrollTop);
|
|
196
209
|
const { width: vw, height: vh } = viewportSizeRef.current;
|
|
197
210
|
resizeLayoutAnchorRef.current = {
|
|
198
211
|
viewportWidth: vw,
|
|
@@ -208,25 +221,22 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
208
221
|
zoomX: nextZoom.x,
|
|
209
222
|
zoomY: nextZoom.y,
|
|
210
223
|
};
|
|
211
|
-
zoomRef.current = nextZoom;
|
|
212
224
|
onZoomChange({
|
|
213
225
|
x: Number(nextZoom.x.toFixed(4)),
|
|
214
226
|
y: Number(nextZoom.y.toFixed(4)),
|
|
215
227
|
});
|
|
216
|
-
requestAnimationFrame(() => {
|
|
217
|
-
syncingZoomRef.current = false;
|
|
218
|
-
});
|
|
219
228
|
};
|
|
220
229
|
|
|
221
230
|
el.addEventListener("wheel", onWheelZoom, { passive: false, capture: true });
|
|
222
231
|
return () => {
|
|
223
232
|
el.removeEventListener("wheel", onWheelZoom, true);
|
|
224
233
|
};
|
|
225
|
-
}, [
|
|
234
|
+
}, [applyCanvasTransform, onZoomChange, viewport.width, viewport.height]);
|
|
226
235
|
|
|
227
236
|
const applyViewportResizeZoom = useCallback(
|
|
228
237
|
(w: number, h: number) => {
|
|
229
238
|
if (w < 8 || h < 8) return;
|
|
239
|
+
if (syncingZoomRef.current) return;
|
|
230
240
|
|
|
231
241
|
const viewer = viewerRef.current as {
|
|
232
242
|
scrollTo?: (x: number, y: number) => void;
|
|
@@ -285,8 +295,9 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
285
295
|
const nextZoom = { x: nextZoomValue, y: nextZoomValue };
|
|
286
296
|
|
|
287
297
|
syncingZoomRef.current = true;
|
|
298
|
+
zoomRef.current = nextZoom;
|
|
299
|
+
applyCanvasTransform(nextZoomValue);
|
|
288
300
|
viewer.scrollTo?.(nextScrollLeft, nextScrollTop);
|
|
289
|
-
commitScroll(nextScrollLeft, nextScrollTop);
|
|
290
301
|
resizeLayoutAnchorRef.current = {
|
|
291
302
|
viewportWidth: w,
|
|
292
303
|
viewportHeight: h,
|
|
@@ -301,16 +312,12 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
301
312
|
zoomX: nextZoomValue,
|
|
302
313
|
zoomY: nextZoomValue,
|
|
303
314
|
};
|
|
304
|
-
zoomRef.current = nextZoom;
|
|
305
315
|
onZoomChange({
|
|
306
316
|
x: Number(nextZoomValue.toFixed(4)),
|
|
307
317
|
y: Number(nextZoomValue.toFixed(4)),
|
|
308
318
|
});
|
|
309
|
-
requestAnimationFrame(() => {
|
|
310
|
-
syncingZoomRef.current = false;
|
|
311
|
-
});
|
|
312
319
|
},
|
|
313
|
-
[
|
|
320
|
+
[applyCanvasTransform, onZoomChange]
|
|
314
321
|
);
|
|
315
322
|
|
|
316
323
|
const syncViewportElement = useCallback(
|
|
@@ -326,11 +333,14 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
326
333
|
const syncCanvasElement = useCallback(
|
|
327
334
|
(el: HTMLDivElement | null) => {
|
|
328
335
|
canvasElRef.current = el;
|
|
336
|
+
if (el) {
|
|
337
|
+
applyCanvasTransform(uniformViewportZoom(zoomRef.current));
|
|
338
|
+
}
|
|
329
339
|
if (!canvasRef) return;
|
|
330
340
|
if (typeof canvasRef === "function") canvasRef(el);
|
|
331
341
|
else (canvasRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
332
342
|
},
|
|
333
|
-
[canvasRef]
|
|
343
|
+
[applyCanvasTransform, canvasRef]
|
|
334
344
|
);
|
|
335
345
|
|
|
336
346
|
useEffect(() => {
|
|
@@ -465,15 +475,9 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
465
475
|
const nextLeft = panRef.current.startLeft - dx;
|
|
466
476
|
const nextTop = panRef.current.startTop - dy;
|
|
467
477
|
viewer.scrollTo(nextLeft, nextTop);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
: nextLeft;
|
|
472
|
-
const top =
|
|
473
|
-
typeof viewer.getScrollTop === "function"
|
|
474
|
-
? viewer.getScrollTop()
|
|
475
|
-
: nextTop;
|
|
476
|
-
commitScroll(left, top);
|
|
478
|
+
lastScrollRef.current = { left: nextLeft, top: nextTop };
|
|
479
|
+
onScrollChange?.({ left: nextLeft, top: nextTop });
|
|
480
|
+
notifyViewportSync();
|
|
477
481
|
};
|
|
478
482
|
|
|
479
483
|
const up = (ev: MouseEvent) => {
|
|
@@ -498,7 +502,7 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
498
502
|
el.removeEventListener("contextmenu", onContextMenu, true);
|
|
499
503
|
el.removeEventListener("mousedown", onMouseDown, true);
|
|
500
504
|
};
|
|
501
|
-
}, [clearWindowPanListeners, commitScroll]);
|
|
505
|
+
}, [clearWindowPanListeners, commitScroll, notifyViewportSync, onScrollChange]);
|
|
502
506
|
|
|
503
507
|
const resolvedContentSize = useMemo(() => {
|
|
504
508
|
if (contentSize) return contentSize;
|
|
@@ -511,6 +515,35 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
511
515
|
|
|
512
516
|
const canvasScale = useMemo(() => uniformViewportZoom(zoom), [zoom.x, zoom.y]);
|
|
513
517
|
|
|
518
|
+
const reconcileCanvasTransform = useCallback(() => {
|
|
519
|
+
const scale = syncingZoomRef.current
|
|
520
|
+
? uniformViewportZoom(zoomRef.current)
|
|
521
|
+
: uniformViewportZoom(zoom);
|
|
522
|
+
applyCanvasTransform(scale);
|
|
523
|
+
}, [applyCanvasTransform, zoom.x, zoom.y]);
|
|
524
|
+
|
|
525
|
+
useLayoutEffect(() => {
|
|
526
|
+
reconcileCanvasTransform();
|
|
527
|
+
}, [canvasScale, reconcileCanvasTransform]);
|
|
528
|
+
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
if (
|
|
531
|
+
Math.abs(zoom.x - zoomRef.current.x) < 0.0001 &&
|
|
532
|
+
Math.abs(zoom.y - zoomRef.current.y) < 0.0001
|
|
533
|
+
) {
|
|
534
|
+
syncingZoomRef.current = false;
|
|
535
|
+
}
|
|
536
|
+
}, [zoom.x, zoom.y]);
|
|
537
|
+
|
|
538
|
+
const worldStyle = useMemo<React.CSSProperties>(
|
|
539
|
+
() => ({
|
|
540
|
+
width: resolvedContentSize.width,
|
|
541
|
+
height: resolvedContentSize.height,
|
|
542
|
+
position: "relative",
|
|
543
|
+
}),
|
|
544
|
+
[resolvedContentSize.height, resolvedContentSize.width]
|
|
545
|
+
);
|
|
546
|
+
|
|
514
547
|
const style = useMemo<React.CSSProperties>(
|
|
515
548
|
() => ({
|
|
516
549
|
width: resolvedContentSize.width,
|
|
@@ -518,10 +551,9 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
518
551
|
minWidth: "100%",
|
|
519
552
|
minHeight: "100%",
|
|
520
553
|
position: "relative",
|
|
521
|
-
transform: `scale(${canvasScale})`,
|
|
522
554
|
transformOrigin: "0 0",
|
|
523
555
|
}),
|
|
524
|
-
[
|
|
556
|
+
[resolvedContentSize.height, resolvedContentSize.width]
|
|
525
557
|
);
|
|
526
558
|
|
|
527
559
|
const hasMaterialPayload = useCallback((types: ArrayLike<string> | null | undefined) => {
|
|
@@ -613,44 +645,29 @@ export const PanelCanvas = React.forwardRef<HTMLDivElement, PanelCanvasProps>(
|
|
|
613
645
|
const next = { left: e.scrollLeft ?? 0, top: e.scrollTop ?? 0 };
|
|
614
646
|
lastScrollRef.current = next;
|
|
615
647
|
onScrollChange?.(next);
|
|
648
|
+
if (!syncingZoomRef.current) {
|
|
649
|
+
notifyViewportSync();
|
|
650
|
+
}
|
|
616
651
|
}}
|
|
617
652
|
>
|
|
618
|
-
<
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
pinch={{ disabled: true }}
|
|
628
|
-
wheel={{ step: 0.008, disabled: true, wheelDisabled: true }}
|
|
629
|
-
centerZoomedOut={false}
|
|
630
|
-
>
|
|
631
|
-
<TransformComponent
|
|
632
|
-
// 始终占满操作容器,但不在这一层裁切放大后的内容
|
|
633
|
-
wrapperStyle={{ width: "100%", height: "100%", overflow: "visible" }}
|
|
634
|
-
contentStyle={{ width: "100%", height: "100%", overflow: "visible" }}
|
|
653
|
+
<div style={worldStyle}>
|
|
654
|
+
<div
|
|
655
|
+
ref={syncCanvasElement}
|
|
656
|
+
data-panel-canvas
|
|
657
|
+
style={style}
|
|
658
|
+
className="h-full w-full"
|
|
659
|
+
onMouseDownCapture={onCanvasMouseDownCapture}
|
|
660
|
+
onClickCapture={onCanvasClickCapture}
|
|
661
|
+
onContextMenuCapture={onCanvasContextMenuCapture}
|
|
635
662
|
>
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
onMouseDownCapture={onCanvasMouseDownCapture}
|
|
642
|
-
onClickCapture={onCanvasClickCapture}
|
|
643
|
-
onContextMenuCapture={onCanvasContextMenuCapture}
|
|
644
|
-
>
|
|
645
|
-
{children}
|
|
646
|
-
{viewportOverlay ? (
|
|
647
|
-
<div className="rv-viewport-overlay pointer-events-none absolute inset-0 z-[70] overflow-visible [&_.moveable-control-box]:pointer-events-auto">
|
|
648
|
-
{viewportOverlay}
|
|
649
|
-
</div>
|
|
650
|
-
) : null}
|
|
663
|
+
{children}
|
|
664
|
+
</div>
|
|
665
|
+
{viewportOverlay ? (
|
|
666
|
+
<div className="rv-viewport-overlay pointer-events-none absolute inset-0 z-[70] overflow-visible [&_.moveable-control-box]:pointer-events-auto [&_.moveable-group]:pointer-events-auto [&_.moveable-area]:pointer-events-auto">
|
|
667
|
+
{viewportOverlay}
|
|
651
668
|
</div>
|
|
652
|
-
|
|
653
|
-
</
|
|
669
|
+
) : null}
|
|
670
|
+
</div>
|
|
654
671
|
</InfiniteViewer>
|
|
655
672
|
);
|
|
656
673
|
}
|
|
@@ -19,10 +19,6 @@ import {
|
|
|
19
19
|
SelectTrigger,
|
|
20
20
|
SelectValue,
|
|
21
21
|
Textarea,
|
|
22
|
-
Tooltip,
|
|
23
|
-
TooltipContent,
|
|
24
|
-
TooltipProvider,
|
|
25
|
-
TooltipTrigger,
|
|
26
22
|
} from "@arronqzy/ui";
|
|
27
23
|
import type {
|
|
28
24
|
PanelChartConfig,
|
|
@@ -30,10 +26,11 @@ import type {
|
|
|
30
26
|
PanelElementStyle,
|
|
31
27
|
ReferenceCopyMode,
|
|
32
28
|
} from "../types";
|
|
33
|
-
import { buildChartOption, CHART_TYPES, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
|
|
29
|
+
import { buildChartOption, CHART_TYPES, getChartLabelsDisplayText, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
|
|
34
30
|
import type { PanelLayer } from "../types";
|
|
35
31
|
import { PANEL_MESSAGES } from "../constants/messages";
|
|
36
32
|
import { ViewElementScopePanel } from "./ViewElementScopePanel";
|
|
33
|
+
import { ConfigHintIcon } from "./ConfigHintIcon";
|
|
37
34
|
import { hasViewElementScope } from "../scope/view-scope-store";
|
|
38
35
|
import { collectElementScopeWarnings } from "../utils/scope-template-warnings";
|
|
39
36
|
import { ScopeConfigProvider } from "./scope-config/ScopeConfigContext";
|
|
@@ -415,7 +412,8 @@ export function PanelConfigSidebar({
|
|
|
415
412
|
title: string,
|
|
416
413
|
children: React.ReactNode,
|
|
417
414
|
defaultOpen = true,
|
|
418
|
-
searchTerms: string[] = []
|
|
415
|
+
searchTerms: string[] = [],
|
|
416
|
+
hint?: React.ReactNode
|
|
419
417
|
) => {
|
|
420
418
|
const hit =
|
|
421
419
|
!hasSearch ||
|
|
@@ -437,7 +435,10 @@ export function PanelConfigSidebar({
|
|
|
437
435
|
{isSectionExpanded(key, defaultOpen) ? "▾" : "▸"}
|
|
438
436
|
</button>
|
|
439
437
|
</CollapsibleTrigger>
|
|
440
|
-
<div className="
|
|
438
|
+
<div className="flex min-w-0 flex-1 items-center gap-1">
|
|
439
|
+
<div className="text-[11px] font-semibold tracking-wide text-muted-foreground">{title}</div>
|
|
440
|
+
{hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
|
|
441
|
+
</div>
|
|
441
442
|
</div>
|
|
442
443
|
<CollapsibleContent className="space-y-3 border-t border-border/60 bg-muted/[0.1] px-3 pb-3 pt-2.5">
|
|
443
444
|
{children}
|
|
@@ -446,9 +447,16 @@ export function PanelConfigSidebar({
|
|
|
446
447
|
);
|
|
447
448
|
};
|
|
448
449
|
|
|
449
|
-
const renderFieldGroup = (
|
|
450
|
+
const renderFieldGroup = (
|
|
451
|
+
title: string,
|
|
452
|
+
children: React.ReactNode,
|
|
453
|
+
hint?: React.ReactNode
|
|
454
|
+
) => (
|
|
450
455
|
<div className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5">
|
|
451
|
-
<div className="
|
|
456
|
+
<div className="flex items-center gap-1">
|
|
457
|
+
<div className="text-[11px] font-semibold text-muted-foreground">{title}</div>
|
|
458
|
+
{hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
|
|
459
|
+
</div>
|
|
452
460
|
{children}
|
|
453
461
|
</div>
|
|
454
462
|
);
|
|
@@ -494,50 +502,24 @@ export function PanelConfigSidebar({
|
|
|
494
502
|
const renderOptionLabel = (label: string, keyPath: string, desc: string) => (
|
|
495
503
|
<div className="flex min-w-0 items-center gap-1">
|
|
496
504
|
<span className={optionLabelTextClass} title={`${label}(${keyPath})`}>{label}</span>
|
|
497
|
-
<
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
type="button"
|
|
502
|
-
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] text-muted-foreground"
|
|
503
|
-
aria-label={`${label}说明`}
|
|
504
|
-
>
|
|
505
|
-
?
|
|
506
|
-
</button>
|
|
507
|
-
</TooltipTrigger>
|
|
508
|
-
<TooltipContent className="max-w-[260px] text-[11px]">
|
|
509
|
-
<div>Key: {keyPath}</div>
|
|
510
|
-
<div>{desc}</div>
|
|
511
|
-
</TooltipContent>
|
|
512
|
-
</Tooltip>
|
|
513
|
-
</TooltipProvider>
|
|
505
|
+
<ConfigHintIcon label={label}>
|
|
506
|
+
<div>Key: {keyPath}</div>
|
|
507
|
+
<div>{desc}</div>
|
|
508
|
+
</ConfigHintIcon>
|
|
514
509
|
</div>
|
|
515
510
|
);
|
|
516
511
|
const renderFormatterLabel = (label = "Tooltip Formatter(可选)") => (
|
|
517
512
|
<div className="flex min-w-0 items-center gap-1">
|
|
518
513
|
<span className={optionLabelTextClass} title={label}>{label}</span>
|
|
519
|
-
<
|
|
520
|
-
<
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
</button>
|
|
529
|
-
</TooltipTrigger>
|
|
530
|
-
<TooltipContent className="max-w-[360px] text-[11px] leading-5">
|
|
531
|
-
<div className="font-medium">可用占位符</div>
|
|
532
|
-
<div>{"{a}=系列名, {b}=类目名, {c}=数值, {d}=百分比(饼图)"}</div>
|
|
533
|
-
<div className="mt-1 font-medium">常用模板 + 输出示例</div>
|
|
534
|
-
<div>{"1) {b}: {c} -> 周一: 120"}</div>
|
|
535
|
-
<div>{"2) {a}<br/>{b}: {c} -> 销量<br/>周一: 120"}</div>
|
|
536
|
-
<div>{"3) {b}: {c} ({d}%) -> 访问来源: 335 (42%)"}</div>
|
|
537
|
-
<div>{"4) ¥{c} / {b} -> ¥120 / 周一"}</div>
|
|
538
|
-
</TooltipContent>
|
|
539
|
-
</Tooltip>
|
|
540
|
-
</TooltipProvider>
|
|
514
|
+
<ConfigHintIcon label="Tooltip Formatter" contentClassName="max-w-[360px]">
|
|
515
|
+
<div className="font-medium">可用占位符</div>
|
|
516
|
+
<div>{"{a}=系列名, {b}=类目名, {c}=数值, {d}=百分比(饼图)"}</div>
|
|
517
|
+
<div className="mt-1 font-medium">常用模板 + 输出示例</div>
|
|
518
|
+
<div>{"1) {b}: {c} -> 周一: 120"}</div>
|
|
519
|
+
<div>{"2) {a}<br/>{b}: {c} -> 销量<br/>周一: 120"}</div>
|
|
520
|
+
<div>{"3) {b}: {c} ({d}%) -> 访问来源: 335 (42%)"}</div>
|
|
521
|
+
<div>{"4) ¥{c} / {b} -> ¥120 / 周一"}</div>
|
|
522
|
+
</ConfigHintIcon>
|
|
541
523
|
</div>
|
|
542
524
|
);
|
|
543
525
|
const mergeOptionPatch = useCallback(
|
|
@@ -593,6 +575,7 @@ export function PanelConfigSidebar({
|
|
|
593
575
|
return (
|
|
594
576
|
<ScopeConfigProvider
|
|
595
577
|
scope={showScopePanel ? viewElementScope : undefined}
|
|
578
|
+
element={selectedElement}
|
|
596
579
|
warnings={scopeWarnings}
|
|
597
580
|
scrollContainerRef={sidebarScrollRef}
|
|
598
581
|
>
|
|
@@ -1102,15 +1085,12 @@ export function PanelConfigSidebar({
|
|
|
1102
1085
|
<div>类目(逗号分隔)</div>
|
|
1103
1086
|
<Input
|
|
1104
1087
|
className="h-7"
|
|
1105
|
-
value={(el.chart
|
|
1088
|
+
value={getChartLabelsDisplayText(el.chart)}
|
|
1106
1089
|
onChange={(e) =>
|
|
1107
1090
|
updateElement(el.id, {
|
|
1108
1091
|
chart: {
|
|
1109
1092
|
...(el.chart ?? {}),
|
|
1110
|
-
|
|
1111
|
-
.split(",")
|
|
1112
|
-
.map((s) => s.trim())
|
|
1113
|
-
.filter(Boolean),
|
|
1093
|
+
labelsText: e.target.value,
|
|
1114
1094
|
},
|
|
1115
1095
|
})
|
|
1116
1096
|
}
|
|
@@ -2364,13 +2344,10 @@ export function PanelConfigSidebar({
|
|
|
2364
2344
|
<label className="block space-y-1">
|
|
2365
2345
|
<div>类目(逗号分隔)</div>
|
|
2366
2346
|
<Input
|
|
2367
|
-
value={(selectedElement.chart
|
|
2347
|
+
value={getChartLabelsDisplayText(selectedElement.chart)}
|
|
2368
2348
|
onChange={(e) =>
|
|
2369
2349
|
updateSelectedChart({
|
|
2370
|
-
|
|
2371
|
-
.split(",")
|
|
2372
|
-
.map((s) => s.trim())
|
|
2373
|
-
.filter(Boolean),
|
|
2350
|
+
labelsText: e.target.value,
|
|
2374
2351
|
})
|
|
2375
2352
|
}
|
|
2376
2353
|
className="h-7"
|
|
@@ -2846,10 +2823,10 @@ export function PanelConfigSidebar({
|
|
|
2846
2823
|
onCheckedChange={(checked) => setIsAdvancedOptionMode(checked === true)}
|
|
2847
2824
|
/>
|
|
2848
2825
|
<span>开启高级模式(直接编辑图表 JSON 配置)</span>
|
|
2826
|
+
<ConfigHintIcon label="图表 JSON 高级模式">
|
|
2827
|
+
基础配置会先生成图表配置,高级模式会在此基础上覆盖(深度合并)。
|
|
2828
|
+
</ConfigHintIcon>
|
|
2849
2829
|
</label>
|
|
2850
|
-
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
2851
|
-
基础配置会先生成图表配置,高级模式会在此基础上覆盖(深度合并)。
|
|
2852
|
-
</div>
|
|
2853
2830
|
{isAdvancedOptionMode ? (
|
|
2854
2831
|
<>
|
|
2855
2832
|
<Textarea
|
|
@@ -3241,8 +3218,11 @@ export function PanelConfigSidebar({
|
|
|
3241
3218
|
className="h-20 w-full rounded border border-border/60 object-cover"
|
|
3242
3219
|
/>
|
|
3243
3220
|
) : null}
|
|
3244
|
-
<div className="
|
|
3245
|
-
|
|
3221
|
+
<div className="flex items-center gap-1">
|
|
3222
|
+
<div className="text-[11px] text-muted-foreground">音频占位图</div>
|
|
3223
|
+
<ConfigHintIcon label="音频占位图">
|
|
3224
|
+
设置占位图或图标后,节点上将隐藏进度条,改为点击图标播放/暂停。
|
|
3225
|
+
</ConfigHintIcon>
|
|
3246
3226
|
</div>
|
|
3247
3227
|
</>
|
|
3248
3228
|
)}
|
|
@@ -3358,12 +3338,10 @@ export function PanelConfigSidebar({
|
|
|
3358
3338
|
</label>
|
|
3359
3339
|
</div>
|
|
3360
3340
|
)}
|
|
3361
|
-
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3362
|
-
网格子节点可跨越多格,占据更大区域,便于复杂布局。
|
|
3363
|
-
</div>
|
|
3364
3341
|
</>,
|
|
3365
3342
|
true,
|
|
3366
|
-
["网格", "跨列", "跨行", "span", "slot"]
|
|
3343
|
+
["网格", "跨列", "跨行", "span", "slot"],
|
|
3344
|
+
<>网格子节点可跨越多格,占据更大区域,便于复杂布局。</>
|
|
3367
3345
|
)
|
|
3368
3346
|
: null
|
|
3369
3347
|
: null}
|
|
@@ -3409,9 +3387,6 @@ export function PanelConfigSidebar({
|
|
|
3409
3387
|
{renderFieldGroup(
|
|
3410
3388
|
"高级(Canvas 脚本)",
|
|
3411
3389
|
<>
|
|
3412
|
-
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3413
|
-
可输入 Canvas 绘制逻辑,变量:ctx、width、height、element。脚本异常会被安全忽略。
|
|
3414
|
-
</div>
|
|
3415
3390
|
<Textarea
|
|
3416
3391
|
value={selectedElement.geometryScript ?? ""}
|
|
3417
3392
|
onChange={(e) => updateSelectedGeometry({ geometryScript: e.target.value || undefined })}
|
|
@@ -3419,6 +3394,9 @@ export function PanelConfigSidebar({
|
|
|
3419
3394
|
className="h-36 font-mono text-[11px]"
|
|
3420
3395
|
placeholder="// 例: ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fillRect(8,8,width-16,height-16);"
|
|
3421
3396
|
/>
|
|
3397
|
+
</>,
|
|
3398
|
+
<>
|
|
3399
|
+
可输入 Canvas 绘制逻辑,变量:ctx、width、height、element。脚本异常会被安全忽略。
|
|
3422
3400
|
</>
|
|
3423
3401
|
)}
|
|
3424
3402
|
{renderFieldGroup(
|
|
@@ -3610,12 +3588,12 @@ export function PanelConfigSidebar({
|
|
|
3610
3588
|
</label>
|
|
3611
3589
|
</div>
|
|
3612
3590
|
)}
|
|
3613
|
-
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3614
|
-
其他节点拖拽靠近该网格槽位中心时会自动吸附,并在节点树显示为该网格子节点。
|
|
3615
|
-
</div>
|
|
3616
3591
|
</>,
|
|
3617
3592
|
true,
|
|
3618
|
-
["网格", "grid", "行", "列", "间距", "内边距", "吸附", "阈值"]
|
|
3593
|
+
["网格", "grid", "行", "列", "间距", "内边距", "吸附", "阈值"],
|
|
3594
|
+
<>
|
|
3595
|
+
其他节点拖拽靠近该网格槽位中心时会自动吸附,并在节点树显示为该网格子节点。
|
|
3596
|
+
</>
|
|
3619
3597
|
)
|
|
3620
3598
|
) : selectedElement.materialType === "reference" ? (
|
|
3621
3599
|
renderSection(
|
|
@@ -3673,9 +3651,9 @@ export function PanelConfigSidebar({
|
|
|
3673
3651
|
</SelectContent>
|
|
3674
3652
|
</Select>
|
|
3675
3653
|
</label>
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3654
|
+
</>,
|
|
3655
|
+
<>
|
|
3656
|
+
浅拷贝会实时同步被引用图层;深拷贝会固定当前快照,不再随源变化。
|
|
3679
3657
|
</>
|
|
3680
3658
|
)}
|
|
3681
3659
|
</>,
|
|
@@ -74,6 +74,7 @@ export function SelectLayer({
|
|
|
74
74
|
if (selectedIdSet.size > 0) {
|
|
75
75
|
if (
|
|
76
76
|
target?.closest(".moveable-control-box") ||
|
|
77
|
+
target?.closest(".moveable-group") ||
|
|
77
78
|
target?.closest(".moveable-line") ||
|
|
78
79
|
target?.closest(".moveable-control") ||
|
|
79
80
|
target?.closest(".moveable-direction")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { formatViewElementScope } from "../utils/scope-template";
|
|
4
|
+
import { ConfigHintIcon } from "./ConfigHintIcon";
|
|
5
|
+
import { ScopeTemplateUsageHint } from "./scope-config/ScopeTemplateUsageHint";
|
|
4
6
|
|
|
5
7
|
const SCOPE_COLLAPSE_STORAGE_KEY = "panel:config-scope-collapsed";
|
|
6
8
|
|
|
@@ -43,6 +45,9 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
|
43
45
|
<div className="flex items-center justify-between gap-2">
|
|
44
46
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
45
47
|
<div className="text-[11px] font-medium text-foreground">Scope 数据</div>
|
|
48
|
+
<ConfigHintIcon label="Scope 模版" contentClassName="max-w-[380px]">
|
|
49
|
+
<ScopeTemplateUsageHint />
|
|
50
|
+
</ConfigHintIcon>
|
|
46
51
|
{scopeUpdated ? (
|
|
47
52
|
<span className="shrink-0 rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:text-sky-300">
|
|
48
53
|
已更新
|
|
@@ -64,14 +69,6 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
|
64
69
|
>
|
|
65
70
|
{formatViewElementScope(scope)}
|
|
66
71
|
</pre>
|
|
67
|
-
<p className="mt-1.5 text-[10px] leading-relaxed text-muted-foreground">
|
|
68
|
-
表单中可使用模版字符串引用 scope,例如{" "}
|
|
69
|
-
<code className="rounded bg-muted px-1">{`{scope?.a||0}`}</code>
|
|
70
|
-
、
|
|
71
|
-
<code className="rounded bg-muted px-1">{`{scope?.name||''}`}</code>
|
|
72
|
-
。支持 <code className="rounded bg-muted px-1">?.</code> 与{" "}
|
|
73
|
-
<code className="rounded bg-muted px-1">||</code> 占位。
|
|
74
|
-
</p>
|
|
75
72
|
</>
|
|
76
73
|
) : null}
|
|
77
74
|
</div>
|
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
type RefObject,
|
|
9
9
|
} from "react";
|
|
10
10
|
|
|
11
|
+
import type { PanelElement } from "../../types";
|
|
11
12
|
import type { ScopeTemplateWarning } from "../../utils/scope-template-warnings";
|
|
12
13
|
import { scopeFieldDomId } from "../../utils/scope-field-labels";
|
|
13
14
|
import { groupScopeWarningsByField } from "../../utils/scope-template-warnings";
|
|
14
15
|
|
|
15
16
|
import { ScopeTemplateAutocompleteHost } from "./ScopeTemplateAutocompleteHost";
|
|
17
|
+
import { ScopeTemplatePreviewHost } from "./ScopeTemplatePreviewHost";
|
|
16
18
|
|
|
17
19
|
type ScopeConfigContextValue = {
|
|
18
20
|
warnings: ScopeTemplateWarning[];
|
|
@@ -83,11 +85,13 @@ function attachInlineHints(
|
|
|
83
85
|
|
|
84
86
|
export function ScopeConfigProvider({
|
|
85
87
|
scope,
|
|
88
|
+
element,
|
|
86
89
|
warnings,
|
|
87
90
|
scrollContainerRef,
|
|
88
91
|
children,
|
|
89
92
|
}: {
|
|
90
93
|
scope?: unknown;
|
|
94
|
+
element?: PanelElement | null;
|
|
91
95
|
warnings: ScopeTemplateWarning[];
|
|
92
96
|
scrollContainerRef?: RefObject<HTMLElement | null>;
|
|
93
97
|
children: ReactNode;
|
|
@@ -156,6 +160,11 @@ export function ScopeConfigProvider({
|
|
|
156
160
|
scope={scope}
|
|
157
161
|
containerRef={scrollContainerRef}
|
|
158
162
|
/>
|
|
163
|
+
<ScopeTemplatePreviewHost
|
|
164
|
+
scope={scope}
|
|
165
|
+
element={element ?? null}
|
|
166
|
+
containerRef={scrollContainerRef}
|
|
167
|
+
/>
|
|
159
168
|
</ScopeConfigContext.Provider>
|
|
160
169
|
);
|
|
161
170
|
}
|