@arronqzy/react-view 1.0.3 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/styles.css +555 -15
- package/package.json +14 -7
- package/readme.md +74 -5
- package/src/core/template.tsx +12 -2
- package/src/panel/ReactViewOnlinePreview.tsx +18 -8
- package/src/panel/ReactViewPanel.tsx +403 -177
- package/src/panel/ai/AssistantChatPanel.tsx +895 -0
- package/src/panel/ai/AssistantMarkdown.tsx +256 -0
- package/src/panel/ai/applyAssistantAction.ts +652 -0
- package/src/panel/ai/revealConfigField.ts +137 -0
- package/src/panel/components/ConfigHintIcon.tsx +50 -0
- package/src/panel/components/ElementsLayer.tsx +39 -22
- package/src/panel/components/MaterialSidebar.tsx +157 -109
- package/src/panel/components/MoveableLayer.tsx +75 -61
- package/src/panel/components/PanelCanvas.tsx +81 -64
- package/src/panel/components/PanelConfigSidebar.tsx +471 -448
- package/src/panel/components/SelectLayer.tsx +1 -0
- package/src/panel/components/ViewElementScopePanel.tsx +13 -12
- package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
- package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
- package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
- package/src/panel/components/table/ConditionEditor.tsx +205 -0
- package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
- package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
- package/src/panel/components/table/TableNodeContent.tsx +544 -0
- package/src/panel/constants/messages.ts +76 -34
- package/src/panel/hooks/usePanelElements.ts +64 -52
- package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
- package/src/panel/types.ts +6 -0
- package/src/panel/utils/chartOptionBuilder.ts +12 -1
- package/src/panel/utils/panelElementDefaults.ts +69 -39
- package/src/panel/utils/scope-field-labels.ts +51 -37
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +128 -0
- package/src/panel/utils/scope-template-spread.ts +252 -0
- package/src/panel/utils/scope-template-warnings.ts +131 -11
- package/src/panel/utils/scope-template.ts +105 -3
- package/src/tailwind.css +36 -15
|
@@ -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,9 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import { formatViewElementScope } from "../utils/scope-template";
|
|
5
|
+
import { ConfigHintIcon } from "./ConfigHintIcon";
|
|
6
|
+
import { ScopeTemplateUsageHint } from "./scope-config/ScopeTemplateUsageHint";
|
|
4
7
|
|
|
5
8
|
const SCOPE_COLLAPSE_STORAGE_KEY = "panel:config-scope-collapsed";
|
|
6
9
|
|
|
@@ -8,7 +11,10 @@ export type ViewElementScopePanelProps = {
|
|
|
8
11
|
scope: unknown;
|
|
9
12
|
};
|
|
10
13
|
|
|
11
|
-
export function ViewElementScopePanel({
|
|
14
|
+
export function ViewElementScopePanel({
|
|
15
|
+
scope,
|
|
16
|
+
}: ViewElementScopePanelProps) {
|
|
17
|
+
const { t } = useI18n();
|
|
12
18
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
13
19
|
const [scopeUpdated, setScopeUpdated] = useState(false);
|
|
14
20
|
const prevScopeSerializedRef = useRef<string | null>(null);
|
|
@@ -42,10 +48,13 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
|
42
48
|
<div className="mt-2 border-t border-border/50 pt-2">
|
|
43
49
|
<div className="flex items-center justify-between gap-2">
|
|
44
50
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
45
|
-
<div className="text-[11px] font-medium text-foreground">
|
|
51
|
+
<div className="text-[11px] font-medium text-foreground">{t("panel.scope.dataTitle")}</div>
|
|
52
|
+
<ConfigHintIcon label={t("panel.scope.templateHintLabel")} contentClassName="max-w-[380px]">
|
|
53
|
+
<ScopeTemplateUsageHint />
|
|
54
|
+
</ConfigHintIcon>
|
|
46
55
|
{scopeUpdated ? (
|
|
47
56
|
<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
|
-
|
|
57
|
+
{t("panel.scope.updated")}
|
|
49
58
|
</span>
|
|
50
59
|
) : null}
|
|
51
60
|
</div>
|
|
@@ -54,7 +63,7 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
|
54
63
|
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
55
64
|
onClick={() => setIsCollapsed((prev) => !prev)}
|
|
56
65
|
>
|
|
57
|
-
{isCollapsed ? "
|
|
66
|
+
{isCollapsed ? t("panel.scope.expand") : t("panel.scope.collapse")}
|
|
58
67
|
</button>
|
|
59
68
|
</div>
|
|
60
69
|
{!isCollapsed ? (
|
|
@@ -64,14 +73,6 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
|
64
73
|
>
|
|
65
74
|
{formatViewElementScope(scope)}
|
|
66
75
|
</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
76
|
</>
|
|
76
77
|
) : null}
|
|
77
78
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
BlueprintExecutionLogPanel,
|
|
@@ -82,6 +83,8 @@ export function WorkspaceConfigSidebar({
|
|
|
82
83
|
allViewElements,
|
|
83
84
|
...viewPanelProps
|
|
84
85
|
}: WorkspaceConfigSidebarProps) {
|
|
86
|
+
const { t } = useI18n();
|
|
87
|
+
|
|
85
88
|
const viewElementOptions = useMemo(
|
|
86
89
|
() =>
|
|
87
90
|
allViewElements.map((el) => ({
|
|
@@ -136,9 +139,9 @@ export function WorkspaceConfigSidebar({
|
|
|
136
139
|
<path d="M8 7l8 4M8 17l8-4" />
|
|
137
140
|
</svg>
|
|
138
141
|
</EmptyIcon>
|
|
139
|
-
<EmptyTitle
|
|
142
|
+
<EmptyTitle>{t("panel.workspace.configEmptyTitle")}</EmptyTitle>
|
|
140
143
|
<EmptyDescription>
|
|
141
|
-
|
|
144
|
+
{t("panel.workspace.configEmptyDesc")}
|
|
142
145
|
</EmptyDescription>
|
|
143
146
|
</Empty>
|
|
144
147
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
import {
|
|
3
4
|
AlertDialog,
|
|
4
5
|
AlertDialogAction,
|
|
@@ -52,6 +53,8 @@ export function WorkspaceProjectNav({
|
|
|
52
53
|
onDeleteProject,
|
|
53
54
|
onPreviewProject,
|
|
54
55
|
}: WorkspaceProjectNavProps) {
|
|
56
|
+
const { t } = useI18n();
|
|
57
|
+
|
|
55
58
|
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
|
56
59
|
const [busy, setBusy] = useState(false);
|
|
57
60
|
|
|
@@ -81,11 +84,11 @@ export function WorkspaceProjectNav({
|
|
|
81
84
|
disabled={busy}
|
|
82
85
|
onClick={() => void runAction(onCreateProject)}
|
|
83
86
|
>
|
|
84
|
-
|
|
87
|
+
{t("panel.workspace.create")}
|
|
85
88
|
</Button>
|
|
86
89
|
</TooltipTrigger>
|
|
87
90
|
<TooltipContent className="z-[10100]">
|
|
88
|
-
|
|
91
|
+
{t("panel.workspace.createHint")}
|
|
89
92
|
</TooltipContent>
|
|
90
93
|
</Tooltip>
|
|
91
94
|
|
|
@@ -100,13 +103,17 @@ export function WorkspaceProjectNav({
|
|
|
100
103
|
disabled={busy || !dirty}
|
|
101
104
|
onClick={() => void runAction(onSyncProject)}
|
|
102
105
|
>
|
|
103
|
-
|
|
106
|
+
{dirty ? t("panel.workspace.syncDirty") : t("common.sync")}
|
|
104
107
|
</Button>
|
|
105
108
|
</TooltipTrigger>
|
|
106
109
|
<TooltipContent className="z-[10100]">
|
|
107
110
|
{dirty
|
|
108
|
-
?
|
|
109
|
-
|
|
111
|
+
? t("panel.workspace.syncUpdate", {
|
|
112
|
+
name:
|
|
113
|
+
activeProjectName ??
|
|
114
|
+
t("panel.workspace.currentWorkspaceFallback"),
|
|
115
|
+
})
|
|
116
|
+
: t("panel.workspace.syncUpToDate")}
|
|
110
117
|
</TooltipContent>
|
|
111
118
|
</Tooltip>
|
|
112
119
|
) : null}
|
|
@@ -120,12 +127,14 @@ export function WorkspaceProjectNav({
|
|
|
120
127
|
className="h-7 max-w-[200px] truncate px-2 text-xs"
|
|
121
128
|
disabled={busy}
|
|
122
129
|
>
|
|
123
|
-
{activeProjectName
|
|
130
|
+
{activeProjectName
|
|
131
|
+
? t("panel.workspace.workspaceNamed", { name: activeProjectName })
|
|
132
|
+
: t("panel.workspace.savedWorkspaces")}
|
|
124
133
|
</Button>
|
|
125
134
|
</DropdownMenuTrigger>
|
|
126
135
|
<DropdownMenuContent align="start" className="z-[10100] w-72">
|
|
127
136
|
{projects.length === 0 ? (
|
|
128
|
-
<DropdownMenuItem disabled
|
|
137
|
+
<DropdownMenuItem disabled>{t("panel.workspace.noSavedWorkspaces")}</DropdownMenuItem>
|
|
129
138
|
) : (
|
|
130
139
|
projects.map((project) => (
|
|
131
140
|
<div key={project.id} className="px-1 py-0.5">
|
|
@@ -160,7 +169,7 @@ export function WorkspaceProjectNav({
|
|
|
160
169
|
})
|
|
161
170
|
}
|
|
162
171
|
>
|
|
163
|
-
|
|
172
|
+
{t("panel.workspace.previewDocTitle")}
|
|
164
173
|
</Button>
|
|
165
174
|
<Button
|
|
166
175
|
type="button"
|
|
@@ -169,7 +178,7 @@ export function WorkspaceProjectNav({
|
|
|
169
178
|
className="h-7 shrink-0 px-2 text-[10px] text-destructive hover:text-destructive"
|
|
170
179
|
onClick={() => setPendingDeleteId(project.id)}
|
|
171
180
|
>
|
|
172
|
-
|
|
181
|
+
{t("common.delete")}
|
|
173
182
|
</Button>
|
|
174
183
|
</div>
|
|
175
184
|
</div>
|
|
@@ -177,7 +186,7 @@ export function WorkspaceProjectNav({
|
|
|
177
186
|
)}
|
|
178
187
|
{/* <DropdownMenuSeparator />
|
|
179
188
|
<DropdownMenuItem onClick={() => void runAction(onCreateProject)}>
|
|
180
|
-
|
|
189
|
+
{t("panel.workspace.create")}
|
|
181
190
|
</DropdownMenuItem> */}
|
|
182
191
|
</DropdownMenuContent>
|
|
183
192
|
</DropdownMenu>
|
|
@@ -192,13 +201,15 @@ export function WorkspaceProjectNav({
|
|
|
192
201
|
>
|
|
193
202
|
<AlertDialogContent className="z-[10150]">
|
|
194
203
|
<AlertDialogHeader>
|
|
195
|
-
<AlertDialogTitle
|
|
204
|
+
<AlertDialogTitle>{t("panel.workspace.deleteConfirmTitle")}</AlertDialogTitle>
|
|
196
205
|
<AlertDialogDescription>
|
|
197
|
-
|
|
206
|
+
{t("panel.workspace.deleteConfirmBody", {
|
|
207
|
+
name: pendingDelete?.name ?? "",
|
|
208
|
+
})}
|
|
198
209
|
</AlertDialogDescription>
|
|
199
210
|
</AlertDialogHeader>
|
|
200
211
|
<AlertDialogFooter>
|
|
201
|
-
<AlertDialogCancel
|
|
212
|
+
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
202
213
|
<AlertDialogAction
|
|
203
214
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
204
215
|
onClick={() => {
|
|
@@ -210,7 +221,7 @@ export function WorkspaceProjectNav({
|
|
|
210
221
|
});
|
|
211
222
|
}}
|
|
212
223
|
>
|
|
213
|
-
|
|
224
|
+
{t("common.delete")}
|
|
214
225
|
</AlertDialogAction>
|
|
215
226
|
</AlertDialogFooter>
|
|
216
227
|
</AlertDialogContent>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type ComponentRef, type ReactNode } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
BluePrintReactRoot,
|
|
@@ -58,6 +59,7 @@ function BlueprintPanel({
|
|
|
58
59
|
canSyncBlueprint?: boolean;
|
|
59
60
|
blueprintDebug?: BlueprintDebugToolbarProps;
|
|
60
61
|
}) {
|
|
62
|
+
const { t } = useI18n();
|
|
61
63
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
62
64
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
63
65
|
|
|
@@ -104,7 +106,7 @@ function BlueprintPanel({
|
|
|
104
106
|
<BluePrintReactRoot style={{ width: "100%", height: "100%" }} {...blueprintProps} />
|
|
105
107
|
) : (
|
|
106
108
|
<div className="flex h-full items-center justify-center text-[11px] text-muted-foreground">
|
|
107
|
-
{blueprintOpen ? "
|
|
109
|
+
{blueprintOpen ? t("panel.workspace.canvasLoading") : ""}
|
|
108
110
|
</div>
|
|
109
111
|
)}
|
|
110
112
|
</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
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useId,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
type RefObject,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
|
|
12
|
+
import type { PanelElement } from "../../types";
|
|
13
|
+
import { hasScopeTemplate } from "../../utils/scope-template";
|
|
14
|
+
import {
|
|
15
|
+
collectElementScopeTemplateFields,
|
|
16
|
+
formatScopeTemplatePreview,
|
|
17
|
+
matchScopeTemplateFieldId,
|
|
18
|
+
resolveScopeTemplatePreview,
|
|
19
|
+
} from "../../utils/scope-template-preview";
|
|
20
|
+
|
|
21
|
+
const PREVIEW_SLOT_ATTR = "data-scope-preview-slot";
|
|
22
|
+
const PREVIEW_KEY_ATTR = "data-scope-preview-key";
|
|
23
|
+
|
|
24
|
+
type PreviewAnchor = {
|
|
25
|
+
key: string;
|
|
26
|
+
template: string;
|
|
27
|
+
fieldId?: string;
|
|
28
|
+
slot: HTMLElement;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function isPreviewTarget(
|
|
32
|
+
node: Element
|
|
33
|
+
): node is HTMLInputElement | HTMLTextAreaElement {
|
|
34
|
+
if (node instanceof HTMLTextAreaElement) {
|
|
35
|
+
return node.dataset.scopeAutocomplete !== "off";
|
|
36
|
+
}
|
|
37
|
+
if (node instanceof HTMLInputElement) {
|
|
38
|
+
if (node.dataset.scopeAutocomplete === "off") return false;
|
|
39
|
+
const type = node.type;
|
|
40
|
+
return (
|
|
41
|
+
!type ||
|
|
42
|
+
type === "text" ||
|
|
43
|
+
type === "search" ||
|
|
44
|
+
type === "url" ||
|
|
45
|
+
type === "password"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensurePreviewKey(
|
|
52
|
+
input: HTMLInputElement | HTMLTextAreaElement,
|
|
53
|
+
keySeed: string
|
|
54
|
+
): string {
|
|
55
|
+
const existing = input.getAttribute(PREVIEW_KEY_ATTR);
|
|
56
|
+
if (existing) return existing;
|
|
57
|
+
const key = `${keySeed}-${Math.random().toString(36).slice(2, 9)}`;
|
|
58
|
+
input.setAttribute(PREVIEW_KEY_ATTR, key);
|
|
59
|
+
return key;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ensurePreviewSlot(
|
|
63
|
+
input: HTMLInputElement | HTMLTextAreaElement
|
|
64
|
+
): HTMLElement {
|
|
65
|
+
const host = input.closest("label") ?? input.parentElement;
|
|
66
|
+
if (!host) return input;
|
|
67
|
+
|
|
68
|
+
const existing = host.querySelector(`[${PREVIEW_SLOT_ATTR}]`);
|
|
69
|
+
if (existing instanceof HTMLElement) return existing;
|
|
70
|
+
|
|
71
|
+
const slot = document.createElement("div");
|
|
72
|
+
slot.setAttribute(PREVIEW_SLOT_ATTR, "1");
|
|
73
|
+
input.insertAdjacentElement("afterend", slot);
|
|
74
|
+
return slot;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function removeOrphanPreviewSlots(container: HTMLElement) {
|
|
78
|
+
container.querySelectorAll(`[${PREVIEW_SLOT_ATTR}]`).forEach((slot) => {
|
|
79
|
+
const host = slot.parentElement;
|
|
80
|
+
const input = host?.querySelector("input, textarea");
|
|
81
|
+
if (
|
|
82
|
+
!input ||
|
|
83
|
+
!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) ||
|
|
84
|
+
!hasScopeTemplate(input.value)
|
|
85
|
+
) {
|
|
86
|
+
slot.remove();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inferFieldIdFromDom(
|
|
92
|
+
input: HTMLInputElement | HTMLTextAreaElement
|
|
93
|
+
): string | undefined {
|
|
94
|
+
const label = input.closest("label");
|
|
95
|
+
const labelText = label?.textContent ?? "";
|
|
96
|
+
// Match both zh-CN and en-US config labels
|
|
97
|
+
if (/类目|categor|labels/i.test(labelText)) return "chart.labelsText";
|
|
98
|
+
if (/数值|values/i.test(labelText)) return "chart.valuesText";
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ScopeTemplatePreviewPanel({
|
|
103
|
+
template,
|
|
104
|
+
scope,
|
|
105
|
+
fieldId,
|
|
106
|
+
}: {
|
|
107
|
+
template: string;
|
|
108
|
+
scope: unknown;
|
|
109
|
+
fieldId?: string;
|
|
110
|
+
}) {
|
|
111
|
+
const { t } = useI18n();
|
|
112
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
113
|
+
const previewValue = resolveScopeTemplatePreview(template, scope, fieldId);
|
|
114
|
+
const previewText = formatScopeTemplatePreview(previewValue);
|
|
115
|
+
const previewType = Array.isArray(previewValue)
|
|
116
|
+
? t("panel.scope.previewArray", { count: previewValue.length })
|
|
117
|
+
: typeof previewValue;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="mt-1 rounded border border-border/60 bg-muted/30">
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className="flex w-full items-center justify-between gap-2 px-2 py-1 text-left text-[10px] text-muted-foreground hover:bg-accent/40"
|
|
124
|
+
onClick={() => setIsExpanded((prev) => !prev)}
|
|
125
|
+
>
|
|
126
|
+
<span className="font-medium text-foreground/80">{t("panel.scope.previewTitle")}</span>
|
|
127
|
+
<span className="shrink-0 text-[10px]">
|
|
128
|
+
{previewType} · {isExpanded ? t("panel.scope.collapsePreview") : t("panel.scope.expandPreview")}
|
|
129
|
+
</span>
|
|
130
|
+
</button>
|
|
131
|
+
{isExpanded ? (
|
|
132
|
+
<pre className="max-h-28 overflow-auto border-t border-border/50 px-2 py-1.5 font-mono text-[10px] leading-relaxed text-foreground/90 whitespace-pre-wrap break-all">
|
|
133
|
+
{previewText}
|
|
134
|
+
</pre>
|
|
135
|
+
) : null}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function ScopeTemplatePreviewHost({
|
|
141
|
+
scope,
|
|
142
|
+
element,
|
|
143
|
+
containerRef,
|
|
144
|
+
}: {
|
|
145
|
+
scope: unknown;
|
|
146
|
+
element: PanelElement | null;
|
|
147
|
+
containerRef?: RefObject<HTMLElement | null>;
|
|
148
|
+
}) {
|
|
149
|
+
const { t } = useI18n();
|
|
150
|
+
const keySeed = useId();
|
|
151
|
+
const [anchors, setAnchors] = useState<PreviewAnchor[]>([]);
|
|
152
|
+
|
|
153
|
+
const templateFields = useMemo(
|
|
154
|
+
() => (element ? collectElementScopeTemplateFields(element, t) : []),
|
|
155
|
+
[element, t]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const scanAnchors = useCallback(() => {
|
|
159
|
+
const container = containerRef?.current;
|
|
160
|
+
if (!container || scope === undefined) {
|
|
161
|
+
setAnchors((prev) => (prev.length === 0 ? prev : []));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const next: PreviewAnchor[] = [];
|
|
166
|
+
const inputs = container.querySelectorAll("input, textarea");
|
|
167
|
+
for (const node of inputs) {
|
|
168
|
+
if (!isPreviewTarget(node)) continue;
|
|
169
|
+
const template = node.value;
|
|
170
|
+
if (!hasScopeTemplate(template)) continue;
|
|
171
|
+
|
|
172
|
+
const fieldId =
|
|
173
|
+
matchScopeTemplateFieldId(template, templateFields) ??
|
|
174
|
+
inferFieldIdFromDom(node);
|
|
175
|
+
next.push({
|
|
176
|
+
key: ensurePreviewKey(node, keySeed),
|
|
177
|
+
template,
|
|
178
|
+
fieldId,
|
|
179
|
+
slot: ensurePreviewSlot(node),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
removeOrphanPreviewSlots(container);
|
|
184
|
+
setAnchors((prev) => {
|
|
185
|
+
if (
|
|
186
|
+
prev.length === next.length &&
|
|
187
|
+
prev.every(
|
|
188
|
+
(item, index) =>
|
|
189
|
+
item.key === next[index]?.key &&
|
|
190
|
+
item.template === next[index]?.template &&
|
|
191
|
+
item.fieldId === next[index]?.fieldId
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
return prev;
|
|
195
|
+
}
|
|
196
|
+
return next;
|
|
197
|
+
});
|
|
198
|
+
}, [containerRef, keySeed, scope, templateFields]);
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
scanAnchors();
|
|
202
|
+
}, [scanAnchors, element, scope]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const container = containerRef?.current;
|
|
206
|
+
if (!container || scope === undefined) return;
|
|
207
|
+
|
|
208
|
+
const onInput = () => {
|
|
209
|
+
window.requestAnimationFrame(scanAnchors);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
container.addEventListener("input", onInput, true);
|
|
213
|
+
const observer = new MutationObserver(onInput);
|
|
214
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
container.removeEventListener("input", onInput, true);
|
|
218
|
+
observer.disconnect();
|
|
219
|
+
container
|
|
220
|
+
.querySelectorAll(`[${PREVIEW_SLOT_ATTR}]`)
|
|
221
|
+
.forEach((node) => node.remove());
|
|
222
|
+
};
|
|
223
|
+
}, [containerRef, scanAnchors, scope]);
|
|
224
|
+
|
|
225
|
+
if (scope === undefined || anchors.length === 0) return null;
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<>
|
|
229
|
+
{anchors.map((anchor) =>
|
|
230
|
+
createPortal(
|
|
231
|
+
<ScopeTemplatePreviewPanel
|
|
232
|
+
key={anchor.key}
|
|
233
|
+
template={anchor.template}
|
|
234
|
+
scope={scope}
|
|
235
|
+
fieldId={anchor.fieldId}
|
|
236
|
+
/>,
|
|
237
|
+
anchor.slot
|
|
238
|
+
)
|
|
239
|
+
)}
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
|
+
|
|
3
|
+
export function ScopeTemplateUsageHint() {
|
|
4
|
+
const { t } = useI18n();
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div className="space-y-2">
|
|
8
|
+
<p>{t("panel.scope.usageHint1")}</p>
|
|
9
|
+
<p>{t("panel.scope.usageHint2")}</p>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import { useScopeConfig } from "./ScopeConfigContext";
|
|
4
5
|
|
|
5
6
|
const WARNINGS_COLLAPSE_STORAGE_KEY = "panel:config-scope-warnings-collapsed";
|
|
6
7
|
|
|
7
8
|
export function ScopeTemplateWarningsPanel() {
|
|
9
|
+
const { t } = useI18n();
|
|
10
|
+
|
|
8
11
|
const ctx = useScopeConfig();
|
|
9
12
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
10
13
|
|
|
@@ -26,7 +29,7 @@ export function ScopeTemplateWarningsPanel() {
|
|
|
26
29
|
<div className="mt-2 border-t border-border/50 pt-2">
|
|
27
30
|
<div className="flex items-center justify-between gap-2">
|
|
28
31
|
<div className="text-[11px] font-medium text-amber-700 dark:text-amber-300">
|
|
29
|
-
|
|
32
|
+
{t("panel.scope.warningsTitle")}
|
|
30
33
|
<span className="ml-1.5 rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold">
|
|
31
34
|
{ctx.warnings.length}
|
|
32
35
|
</span>
|
|
@@ -36,13 +39,13 @@ export function ScopeTemplateWarningsPanel() {
|
|
|
36
39
|
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
37
40
|
onClick={() => setIsCollapsed((prev) => !prev)}
|
|
38
41
|
>
|
|
39
|
-
{isCollapsed ? "
|
|
42
|
+
{isCollapsed ? t("panel.scope.expandWarnings") : t("panel.scope.collapseWarnings")}
|
|
40
43
|
</button>
|
|
41
44
|
</div>
|
|
42
45
|
{!isCollapsed ? (
|
|
43
46
|
<ul className="mt-2 max-h-[200px] space-y-1 overflow-auto text-[10px] leading-relaxed">
|
|
44
47
|
{ctx.warnings.map((warning) => (
|
|
45
|
-
<li key={`${warning.fieldId}-${warning.missingPath}-${warning.expression}`}>
|
|
48
|
+
<li key={`${warning.fieldId}-${warning.kind}-${warning.missingPath}-${warning.expression}`}>
|
|
46
49
|
<button
|
|
47
50
|
type="button"
|
|
48
51
|
className="w-full rounded border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-left text-amber-800 hover:bg-amber-500/15 dark:text-amber-200"
|