@arronqzy/react-view 1.0.30 → 1.0.32
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 +28 -0
- package/package.json +7 -7
- package/src/panel/ReactViewPanel.tsx +8 -0
- package/src/panel/components/ElementsLayer.tsx +2 -1
- package/src/panel/components/PanelConfigSidebar.tsx +38 -11
- package/src/panel/components/PanelMenubar.tsx +28 -2
- package/src/panel/components/config/PanelConfigAudioSection.tsx +6 -3
- package/src/panel/components/config/PanelConfigChartSection.tsx +10 -2
- package/src/panel/components/config/PanelConfigMultiSelect.tsx +18 -6
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +72 -0
- package/src/panel/components/config/PanelConfigStyleSections.tsx +15 -5
- package/src/panel/components/config/PanelConfigTextSection.tsx +19 -0
- package/src/panel/components/config/PanelConfigVideoSection.tsx +6 -3
- package/src/panel/utils/background-image-style.ts +39 -0
- package/src/panel/utils/scope-template-chart.ts +12 -0
- package/src/panel/utils/scope-template.ts +28 -17
package/dist/styles.css
CHANGED
|
@@ -1150,6 +1150,10 @@ video {
|
|
|
1150
1150
|
min-width: 0px;
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
|
+
.min-w-\[100px\] {
|
|
1154
|
+
min-width: 100px;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1153
1157
|
.min-w-\[120px\] {
|
|
1154
1158
|
min-width: 120px;
|
|
1155
1159
|
}
|
|
@@ -1158,6 +1162,10 @@ video {
|
|
|
1158
1162
|
min-width: 140px;
|
|
1159
1163
|
}
|
|
1160
1164
|
|
|
1165
|
+
.min-w-\[180px\] {
|
|
1166
|
+
min-width: 180px;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1161
1169
|
.min-w-\[92px\] {
|
|
1162
1170
|
min-width: 92px;
|
|
1163
1171
|
}
|
|
@@ -1284,6 +1292,10 @@ video {
|
|
|
1284
1292
|
cursor: crosshair;
|
|
1285
1293
|
}
|
|
1286
1294
|
|
|
1295
|
+
.cursor-default {
|
|
1296
|
+
cursor: default;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1287
1299
|
.cursor-grabbing {
|
|
1288
1300
|
cursor: grabbing;
|
|
1289
1301
|
}
|
|
@@ -1298,6 +1310,12 @@ video {
|
|
|
1298
1310
|
user-select: none;
|
|
1299
1311
|
}
|
|
1300
1312
|
|
|
1313
|
+
.select-text {
|
|
1314
|
+
-webkit-user-select: text;
|
|
1315
|
+
-moz-user-select: text;
|
|
1316
|
+
user-select: text;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1301
1319
|
.resize-none {
|
|
1302
1320
|
resize: none;
|
|
1303
1321
|
}
|
|
@@ -3026,6 +3044,16 @@ video {
|
|
|
3026
3044
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
3027
3045
|
}
|
|
3028
3046
|
|
|
3047
|
+
.focus-visible\:ring-1:focus-visible {
|
|
3048
|
+
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
3049
|
+
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
3050
|
+
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
.focus-visible\:ring-primary:focus-visible {
|
|
3054
|
+
--tw-ring-color: hsl(var(--primary));
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3029
3057
|
.focus-visible\:ring-offset-0:focus-visible {
|
|
3030
3058
|
--tw-ring-offset-width: 0px;
|
|
3031
3059
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/react-view",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"files": [
|
|
5
5
|
"src",
|
|
6
6
|
"dist",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
32
32
|
"@codemirror/view": "^6.36.2",
|
|
33
33
|
"@arronqzy/ui": "1.0.9",
|
|
34
|
-
"@arronqzy/
|
|
35
|
-
"@arronqzy/
|
|
36
|
-
"@arronqzy/react-blueprint": "1.0.
|
|
37
|
-
"@arronqzy/rx-store": "1.0.8",
|
|
34
|
+
"@arronqzy/i18n": "0.1.14",
|
|
35
|
+
"@arronqzy/blueprint-dsl": "1.0.15",
|
|
36
|
+
"@arronqzy/react-blueprint": "1.0.18",
|
|
38
37
|
"@arronqzy/view-scene3d": "0.1.5",
|
|
39
|
-
"@arronqzy/
|
|
40
|
-
"@arronqzy/webllm-assistant": "0.1.16"
|
|
38
|
+
"@arronqzy/rx-store": "1.0.8",
|
|
39
|
+
"@arronqzy/webllm-assistant": "0.1.16",
|
|
40
|
+
"@arronqzy/view-table": "0.1.8"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/react": "^19.0.1",
|
|
@@ -143,6 +143,10 @@ export type ReactViewPanelProps = {
|
|
|
143
143
|
* 省略或空字符串保持历史全局库名。
|
|
144
144
|
*/
|
|
145
145
|
nameSpace?: string | null;
|
|
146
|
+
/** 顶栏「设置 → 关于」展示的 npm 包名,如 @arronqzy/abuilder */
|
|
147
|
+
packageName?: string | null;
|
|
148
|
+
/** 顶栏「设置 → 关于」展示的包版本号 */
|
|
149
|
+
packageVersion?: string | null;
|
|
146
150
|
};
|
|
147
151
|
|
|
148
152
|
function ReactViewPanelInner({
|
|
@@ -150,6 +154,8 @@ function ReactViewPanelInner({
|
|
|
150
154
|
className,
|
|
151
155
|
initialWorkspace = null,
|
|
152
156
|
nameSpace = null,
|
|
157
|
+
packageName = null,
|
|
158
|
+
packageVersion = null,
|
|
153
159
|
}: ReactViewPanelProps) {
|
|
154
160
|
const { t, locale, setLocale } = useI18n();
|
|
155
161
|
const messages = useMemo(() => getPanelMessages(t), [t]);
|
|
@@ -1904,6 +1910,8 @@ function ReactViewPanelInner({
|
|
|
1904
1910
|
setLocale={setLocale}
|
|
1905
1911
|
outputScale={outputScale}
|
|
1906
1912
|
setOutputScale={setOutputScale}
|
|
1913
|
+
packageName={packageName}
|
|
1914
|
+
packageVersion={packageVersion}
|
|
1907
1915
|
/>
|
|
1908
1916
|
<WorkspaceProjectNav
|
|
1909
1917
|
projects={workspaceProjects.projects}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
viewportWindowContentScale,
|
|
14
14
|
} from "../utils/viewportPlacement";
|
|
15
15
|
import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
|
|
16
|
+
import { resolveBackgroundImageStyle } from "../utils/background-image-style";
|
|
16
17
|
import { mergeScene3dConfig } from "@arronqzy/view-scene3d";
|
|
17
18
|
import { Scene3dNodeContent } from "@arronqzy/view-scene3d/react";
|
|
18
19
|
import {
|
|
@@ -660,7 +661,7 @@ function getNodeVisualStyle(element: PanelElement): React.CSSProperties {
|
|
|
660
661
|
const style = element.style ?? {};
|
|
661
662
|
return {
|
|
662
663
|
backgroundColor: style.backgroundColor,
|
|
663
|
-
backgroundImage: style
|
|
664
|
+
backgroundImage: resolveBackgroundImageStyle(style),
|
|
664
665
|
backgroundSize: style.backgroundSize,
|
|
665
666
|
backgroundPosition: style.backgroundPosition,
|
|
666
667
|
borderWidth: style.borderWidth,
|
|
@@ -107,7 +107,7 @@ export function PanelConfigSidebar({
|
|
|
107
107
|
const matchCountRef = useRef(0);
|
|
108
108
|
const themedScrollbarClass =
|
|
109
109
|
"scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
|
|
110
|
-
const
|
|
110
|
+
const DEFAULT_EXPANDED_SECTIONS: Record<string, boolean> = {
|
|
111
111
|
nodeInfo: true,
|
|
112
112
|
styleBackground: false,
|
|
113
113
|
styleBorder: false,
|
|
@@ -117,7 +117,14 @@ export function PanelConfigSidebar({
|
|
|
117
117
|
chartAdvancedLayout: false,
|
|
118
118
|
chartAdvancedAxisPointer: false,
|
|
119
119
|
reference: true,
|
|
120
|
-
}
|
|
120
|
+
};
|
|
121
|
+
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(
|
|
122
|
+
() => ({ ...DEFAULT_EXPANDED_SECTIONS })
|
|
123
|
+
);
|
|
124
|
+
const expandedSectionsByIdRef = useRef(new Map<string, Record<string, boolean>>());
|
|
125
|
+
const expandedSectionsRef = useRef(expandedSections);
|
|
126
|
+
expandedSectionsRef.current = expandedSections;
|
|
127
|
+
const activeExpandedKeyRef = useRef<string | null>(null);
|
|
121
128
|
|
|
122
129
|
const isChartElement = !!selectedElement && CHART_TYPES.has(selectedElement.materialType ?? "");
|
|
123
130
|
const selectedLayer = selectedElement
|
|
@@ -179,14 +186,27 @@ export function PanelConfigSidebar({
|
|
|
179
186
|
el.scrollTop = top;
|
|
180
187
|
}, [configScrollKey]);
|
|
181
188
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
189
|
+
useLayoutEffect(() => {
|
|
190
|
+
const prevKey = activeExpandedKeyRef.current;
|
|
191
|
+
if (prevKey && prevKey !== configScrollKey) {
|
|
192
|
+
expandedSectionsByIdRef.current.set(prevKey, {
|
|
193
|
+
...expandedSectionsRef.current,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
activeExpandedKeyRef.current = configScrollKey;
|
|
197
|
+
if (!configScrollKey) {
|
|
198
|
+
setExpandedSections({ ...DEFAULT_EXPANDED_SECTIONS });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const saved = expandedSectionsByIdRef.current.get(configScrollKey);
|
|
202
|
+
if (saved) {
|
|
203
|
+
setExpandedSections({ ...saved });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const initial = { ...DEFAULT_EXPANDED_SECTIONS };
|
|
207
|
+
expandedSectionsByIdRef.current.set(configScrollKey, initial);
|
|
208
|
+
setExpandedSections(initial);
|
|
209
|
+
}, [configScrollKey]);
|
|
190
210
|
|
|
191
211
|
useEffect(() => {
|
|
192
212
|
if (typeof window === "undefined") return;
|
|
@@ -202,7 +222,12 @@ export function PanelConfigSidebar({
|
|
|
202
222
|
expandedSections[key] ?? defaultValue;
|
|
203
223
|
|
|
204
224
|
const setSectionExpanded = (key: string, next: boolean) => {
|
|
205
|
-
setExpandedSections((prev) =>
|
|
225
|
+
setExpandedSections((prev) => {
|
|
226
|
+
const updated = { ...prev, [key]: next };
|
|
227
|
+
const id = activeExpandedKeyRef.current;
|
|
228
|
+
if (id) expandedSectionsByIdRef.current.set(id, updated);
|
|
229
|
+
return updated;
|
|
230
|
+
});
|
|
206
231
|
};
|
|
207
232
|
|
|
208
233
|
useEffect(() => {
|
|
@@ -220,6 +245,8 @@ export function PanelConfigSidebar({
|
|
|
220
245
|
next.geometryCanvasScript = true;
|
|
221
246
|
next.geometrySketch = true;
|
|
222
247
|
next.referenceCopyStrategy = true;
|
|
248
|
+
const id = activeExpandedKeyRef.current;
|
|
249
|
+
if (id) expandedSectionsByIdRef.current.set(id, next);
|
|
223
250
|
return next;
|
|
224
251
|
});
|
|
225
252
|
window.requestAnimationFrame(() => {
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
MenubarRadioGroup,
|
|
9
9
|
MenubarRadioItem,
|
|
10
10
|
MenubarSeparator,
|
|
11
|
+
MenubarSub,
|
|
12
|
+
MenubarSubContent,
|
|
13
|
+
MenubarSubTrigger,
|
|
11
14
|
MenubarTrigger,
|
|
12
15
|
} from "@arronqzy/ui";
|
|
13
16
|
import type { Locale } from "@arronqzy/i18n";
|
|
@@ -37,6 +40,10 @@ export type PanelMenubarProps = {
|
|
|
37
40
|
setLocale: (locale: Locale) => void;
|
|
38
41
|
outputScale: boolean;
|
|
39
42
|
setOutputScale: (value: boolean) => void;
|
|
43
|
+
/** 产品包名(可选,关于二级菜单不展示) */
|
|
44
|
+
packageName?: string | null;
|
|
45
|
+
/** 产品版本号,如 1.1.32 */
|
|
46
|
+
packageVersion?: string | null;
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
export function PanelMenubar({
|
|
@@ -64,13 +71,18 @@ export function PanelMenubar({
|
|
|
64
71
|
setLocale,
|
|
65
72
|
outputScale,
|
|
66
73
|
setOutputScale,
|
|
74
|
+
packageVersion,
|
|
67
75
|
}: PanelMenubarProps) {
|
|
76
|
+
const versionOnly =
|
|
77
|
+
packageVersion && packageVersion.trim()
|
|
78
|
+
? t("panel.menubar.versionValue", { version: packageVersion.trim() })
|
|
79
|
+
: t("panel.menubar.versionUnavailable");
|
|
80
|
+
|
|
68
81
|
return (
|
|
69
82
|
<Menubar className="h-8 border-0 bg-transparent p-0 shadow-none">
|
|
70
83
|
<MenubarMenu>
|
|
71
84
|
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.file")}</MenubarTrigger>
|
|
72
85
|
<MenubarContent className="z-[10100]">
|
|
73
|
-
{/* <MenubarItem onClick={handlePreviewLayer}>{t("panel.workspace.previewDocTitle")}</MenubarItem> */}
|
|
74
86
|
<MenubarItem onClick={handleExport}>{t("panel.menubar.export")}</MenubarItem>
|
|
75
87
|
<MenubarItem onClick={() => importInputRef.current?.click()}>{t("panel.menubar.import")}</MenubarItem>
|
|
76
88
|
</MenubarContent>
|
|
@@ -113,7 +125,7 @@ export function PanelMenubar({
|
|
|
113
125
|
</MenubarMenu>
|
|
114
126
|
<MenubarMenu>
|
|
115
127
|
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.settings")}</MenubarTrigger>
|
|
116
|
-
<MenubarContent className="z-[10100]">
|
|
128
|
+
<MenubarContent className="z-[10100] min-w-[180px]">
|
|
117
129
|
<MenubarRadioGroup
|
|
118
130
|
value={panelFontSize}
|
|
119
131
|
onValueChange={(value) => setPanelFontSize(value as "sm" | "md" | "lg")}
|
|
@@ -141,6 +153,20 @@ export function PanelMenubar({
|
|
|
141
153
|
>
|
|
142
154
|
{t("panel.menubar.outputScale")}
|
|
143
155
|
</MenubarCheckboxItem>
|
|
156
|
+
<MenubarSeparator />
|
|
157
|
+
<MenubarSub>
|
|
158
|
+
<MenubarSubTrigger>{t("panel.menubar.about")}</MenubarSubTrigger>
|
|
159
|
+
<MenubarSubContent className="z-[10100] min-w-[100px]">
|
|
160
|
+
<MenubarItem
|
|
161
|
+
className="cursor-default select-text"
|
|
162
|
+
onSelect={(e) => e.preventDefault()}
|
|
163
|
+
data-abuilder-version={packageVersion ?? undefined}
|
|
164
|
+
title={`${t("panel.menubar.versionLabel")} ${versionOnly}`}
|
|
165
|
+
>
|
|
166
|
+
{versionOnly}
|
|
167
|
+
</MenubarItem>
|
|
168
|
+
</MenubarSubContent>
|
|
169
|
+
</MenubarSub>
|
|
144
170
|
</MenubarContent>
|
|
145
171
|
</MenubarMenu>
|
|
146
172
|
</Menubar>
|
|
@@ -151,7 +151,7 @@ export function PanelConfigAudioSection({
|
|
|
151
151
|
{renderFieldGroup(
|
|
152
152
|
t("panel.config.groupAudioSource"),
|
|
153
153
|
<>
|
|
154
|
-
<label className="block space-y-1">
|
|
154
|
+
<label className="block space-y-1" data-config-field="audioRemoteUrl">
|
|
155
155
|
<div>{t("panel.config.audioUrl")}</div>
|
|
156
156
|
<Input
|
|
157
157
|
value={selectedElement.audioRemoteUrl ?? ""}
|
|
@@ -161,9 +161,12 @@ export function PanelConfigAudioSection({
|
|
|
161
161
|
audioSrc: e.target.value || selectedElement.audioSrc,
|
|
162
162
|
})
|
|
163
163
|
}
|
|
164
|
-
placeholder="
|
|
165
|
-
className="h-7"
|
|
164
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
165
|
+
className="h-7 font-mono text-[11px]"
|
|
166
166
|
/>
|
|
167
|
+
<p className="text-[10px] text-muted-foreground">
|
|
168
|
+
{t("panel.config.urlScopeHint")}
|
|
169
|
+
</p>
|
|
167
170
|
</label>
|
|
168
171
|
<div className="flex items-center gap-2">
|
|
169
172
|
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
@@ -117,7 +117,7 @@ export function PanelConfigChartSection({
|
|
|
117
117
|
{renderFieldGroup(
|
|
118
118
|
t("panel.config.groupData"),
|
|
119
119
|
<>
|
|
120
|
-
<label className="block space-y-1">
|
|
120
|
+
<label className="block space-y-1" data-config-field="chart.labelsText">
|
|
121
121
|
<div>{t("panel.config.labelsCsv")}</div>
|
|
122
122
|
<Input
|
|
123
123
|
value={getChartLabelsDisplayText(selectedElement.chart)}
|
|
@@ -126,11 +126,15 @@ export function PanelConfigChartSection({
|
|
|
126
126
|
labelsText: e.target.value,
|
|
127
127
|
})
|
|
128
128
|
}
|
|
129
|
+
placeholder={t("panel.config.labelsCsvPlaceholder")}
|
|
129
130
|
className="h-7"
|
|
130
131
|
/>
|
|
132
|
+
<p className="text-[10px] text-muted-foreground">
|
|
133
|
+
{t("panel.config.scopeTemplateHint")}
|
|
134
|
+
</p>
|
|
131
135
|
</label>
|
|
132
136
|
|
|
133
|
-
<label className="block space-y-1">
|
|
137
|
+
<label className="block space-y-1" data-config-field="chart.valuesText">
|
|
134
138
|
<div>{t("panel.config.valuesCsv")}</div>
|
|
135
139
|
<Input
|
|
136
140
|
value={getChartValuesDisplayText(selectedElement.chart)}
|
|
@@ -139,8 +143,12 @@ export function PanelConfigChartSection({
|
|
|
139
143
|
valuesText: e.target.value,
|
|
140
144
|
})
|
|
141
145
|
}
|
|
146
|
+
placeholder={t("panel.config.valuesCsvPlaceholder")}
|
|
142
147
|
className="h-7"
|
|
143
148
|
/>
|
|
149
|
+
<p className="text-[10px] text-muted-foreground">
|
|
150
|
+
{t("panel.config.scopeTemplateHint")}
|
|
151
|
+
</p>
|
|
144
152
|
</label>
|
|
145
153
|
</>
|
|
146
154
|
)}
|
|
@@ -1113,12 +1113,18 @@ export function PanelConfigMultiSelect({
|
|
|
1113
1113
|
) : null}
|
|
1114
1114
|
{el.materialType === "audio" ? (
|
|
1115
1115
|
<div className="grid grid-cols-2 gap-2">
|
|
1116
|
-
<label className="block space-y-1 col-span-2">
|
|
1116
|
+
<label className="block space-y-1 col-span-2" data-config-field="audioRemoteUrl">
|
|
1117
1117
|
<div>{t("panel.config.audioUrl")}</div>
|
|
1118
1118
|
<Input
|
|
1119
|
-
className="h-7"
|
|
1119
|
+
className="h-7 font-mono text-[11px]"
|
|
1120
1120
|
value={el.audioRemoteUrl ?? ""}
|
|
1121
|
-
|
|
1121
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
1122
|
+
onChange={(e) =>
|
|
1123
|
+
updateElement(el.id, {
|
|
1124
|
+
audioRemoteUrl: e.target.value || undefined,
|
|
1125
|
+
audioSrc: e.target.value || el.audioSrc,
|
|
1126
|
+
})
|
|
1127
|
+
}
|
|
1122
1128
|
/>
|
|
1123
1129
|
</label>
|
|
1124
1130
|
<label className="block space-y-1">
|
|
@@ -1156,12 +1162,18 @@ export function PanelConfigMultiSelect({
|
|
|
1156
1162
|
</div>
|
|
1157
1163
|
) : null}
|
|
1158
1164
|
{el.materialType === "video" ? (
|
|
1159
|
-
<label className="block space-y-1">
|
|
1165
|
+
<label className="block space-y-1" data-config-field="videoRemoteUrl">
|
|
1160
1166
|
<div>{t("panel.config.videoUrl")}</div>
|
|
1161
1167
|
<Input
|
|
1162
|
-
className="h-7"
|
|
1168
|
+
className="h-7 font-mono text-[11px]"
|
|
1163
1169
|
value={el.videoRemoteUrl ?? ""}
|
|
1164
|
-
|
|
1170
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
1171
|
+
onChange={(e) =>
|
|
1172
|
+
updateElement(el.id, {
|
|
1173
|
+
videoRemoteUrl: e.target.value || undefined,
|
|
1174
|
+
videoSrc: e.target.value || el.videoSrc,
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1165
1177
|
/>
|
|
1166
1178
|
</label>
|
|
1167
1179
|
) : null}
|
|
@@ -69,6 +69,7 @@ export function PanelConfigScene3dSection({
|
|
|
69
69
|
[element.scene3d]
|
|
70
70
|
);
|
|
71
71
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
72
|
+
const [modelUrlDraft, setModelUrlDraft] = useState("");
|
|
72
73
|
const [nameQuery, setNameQuery] = useState<Record<string, string>>({});
|
|
73
74
|
const [openTagKeys, setOpenTagKeys] = useState<Record<string, boolean>>({});
|
|
74
75
|
const patch = useCallback(
|
|
@@ -96,6 +97,25 @@ export function PanelConfigScene3dSection({
|
|
|
96
97
|
patch({ models: nextModels });
|
|
97
98
|
};
|
|
98
99
|
|
|
100
|
+
const handleAddModelByUrl = () => {
|
|
101
|
+
const url = modelUrlDraft.trim();
|
|
102
|
+
if (!url) return;
|
|
103
|
+
const fileName = url.split(/[/?#]/).filter(Boolean).pop() || "model";
|
|
104
|
+
patch({
|
|
105
|
+
models: [
|
|
106
|
+
...config.models,
|
|
107
|
+
{
|
|
108
|
+
id: randomId("mdl"),
|
|
109
|
+
label: fileName.replace(/\.[^.]+$/, "") || fileName,
|
|
110
|
+
url,
|
|
111
|
+
fileName,
|
|
112
|
+
format: inferModelFormat(fileName),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
setModelUrlDraft("");
|
|
117
|
+
};
|
|
118
|
+
|
|
99
119
|
const updateModelAnimation = (
|
|
100
120
|
id: string,
|
|
101
121
|
next: Partial<Scene3dModelAnimationRule>
|
|
@@ -382,6 +402,30 @@ export function PanelConfigScene3dSection({
|
|
|
382
402
|
}}
|
|
383
403
|
/>
|
|
384
404
|
</div>
|
|
405
|
+
<label className="block space-y-1" data-config-field="scene3d.modelUrl">
|
|
406
|
+
<div className="text-[10px] text-muted-foreground">
|
|
407
|
+
{t("panel.config.scene3dModelUrl")}
|
|
408
|
+
</div>
|
|
409
|
+
<div className="flex gap-1.5">
|
|
410
|
+
<Input
|
|
411
|
+
value={modelUrlDraft}
|
|
412
|
+
onChange={(e) => setModelUrlDraft(e.target.value)}
|
|
413
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
414
|
+
className="h-7 flex-1 font-mono text-[11px]"
|
|
415
|
+
/>
|
|
416
|
+
<Button
|
|
417
|
+
type="button"
|
|
418
|
+
size="sm"
|
|
419
|
+
variant="outline"
|
|
420
|
+
className="h-7 shrink-0 text-[11px]"
|
|
421
|
+
disabled={!modelUrlDraft.trim()}
|
|
422
|
+
onClick={handleAddModelByUrl}
|
|
423
|
+
>
|
|
424
|
+
{t("common.add")}
|
|
425
|
+
</Button>
|
|
426
|
+
</div>
|
|
427
|
+
<p className="text-[10px] text-muted-foreground">{t("panel.config.urlScopeHint")}</p>
|
|
428
|
+
</label>
|
|
385
429
|
|
|
386
430
|
{config.models.length === 0 ? (
|
|
387
431
|
<div className="rounded border border-dashed border-border/70 px-2 py-3 text-[11px] text-muted-foreground">
|
|
@@ -432,6 +476,34 @@ export function PanelConfigScene3dSection({
|
|
|
432
476
|
</Button>
|
|
433
477
|
</div>
|
|
434
478
|
<div className="text-[10px] text-muted-foreground">{model.fileName ?? model.url.slice(0, 48)}</div>
|
|
479
|
+
<label className="block space-y-0.5">
|
|
480
|
+
<span className="text-[10px] text-muted-foreground">
|
|
481
|
+
{t("panel.config.scene3dModelUrl")}
|
|
482
|
+
</span>
|
|
483
|
+
<Input
|
|
484
|
+
value={model.url}
|
|
485
|
+
data-config-field="scene3d.models.url"
|
|
486
|
+
className="h-7 font-mono text-[10px]"
|
|
487
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
488
|
+
onChange={(e) => {
|
|
489
|
+
const nextUrl = e.target.value;
|
|
490
|
+
const fileName =
|
|
491
|
+
nextUrl.split(/[/?#]/).filter(Boolean).pop() || model.fileName || "model";
|
|
492
|
+
patch({
|
|
493
|
+
models: config.models.map((m) =>
|
|
494
|
+
m.id === model.id
|
|
495
|
+
? {
|
|
496
|
+
...m,
|
|
497
|
+
url: nextUrl,
|
|
498
|
+
fileName,
|
|
499
|
+
format: inferModelFormat(fileName),
|
|
500
|
+
}
|
|
501
|
+
: m
|
|
502
|
+
),
|
|
503
|
+
});
|
|
504
|
+
}}
|
|
505
|
+
/>
|
|
506
|
+
</label>
|
|
435
507
|
<div className="grid grid-cols-3 gap-1">
|
|
436
508
|
{(["x", "y", "z"] as const).map((axis, index) => (
|
|
437
509
|
<Input
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@arronqzy/ui";
|
|
11
11
|
import type { PanelElement, PanelElementStyle } from "../../types";
|
|
12
12
|
import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
|
|
13
|
+
import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
|
|
13
14
|
import { getPanelMessages } from "../../constants/messages";
|
|
14
15
|
import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
|
|
15
16
|
|
|
@@ -88,14 +89,23 @@ export function PanelConfigStyleSections({
|
|
|
88
89
|
{renderFieldGroup(
|
|
89
90
|
t("panel.config.groupBgLayout"),
|
|
90
91
|
<>
|
|
91
|
-
<label className="block space-y-1">
|
|
92
|
+
<label className="block space-y-1" data-config-field="style.backgroundImage">
|
|
92
93
|
<div>{t("panel.config.backgroundImage")}</div>
|
|
93
94
|
<Input
|
|
94
|
-
value={
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
value={
|
|
96
|
+
selectedElement.style?.backgroundImageRemoteUrl ??
|
|
97
|
+
selectedElement.style?.backgroundImage ??
|
|
98
|
+
""
|
|
99
|
+
}
|
|
100
|
+
onChange={(e) =>
|
|
101
|
+
updateSelectedStyle(patchBackgroundImageFromInput(e.target.value))
|
|
102
|
+
}
|
|
103
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
104
|
+
className="h-7 font-mono text-[11px]"
|
|
98
105
|
/>
|
|
106
|
+
<p className="text-[10px] text-muted-foreground">
|
|
107
|
+
{t("panel.config.urlScopeHint")}
|
|
108
|
+
</p>
|
|
99
109
|
</label>
|
|
100
110
|
<div className="flex items-center gap-2">
|
|
101
111
|
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
@@ -110,6 +110,25 @@ export function PanelConfigTextSection({
|
|
|
110
110
|
});
|
|
111
111
|
}}
|
|
112
112
|
/>
|
|
113
|
+
<label className="block space-y-1">
|
|
114
|
+
<div>{t("panel.config.textHtmlSource")}</div>
|
|
115
|
+
<textarea
|
|
116
|
+
data-config-field="textHtml"
|
|
117
|
+
value={selectedElement.textHtml ?? ""}
|
|
118
|
+
onChange={(e) =>
|
|
119
|
+
updateSelectedText({
|
|
120
|
+
textHtml: e.target.value || "<p><br/></p>",
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
rows={4}
|
|
124
|
+
spellCheck={false}
|
|
125
|
+
placeholder={t("panel.config.textHtmlSourcePlaceholder")}
|
|
126
|
+
className="w-full rounded border border-border bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed outline-none focus-visible:ring-1 focus-visible:ring-primary"
|
|
127
|
+
/>
|
|
128
|
+
<p className="text-[10px] text-muted-foreground">
|
|
129
|
+
{t("panel.config.scopeTemplateHint")}
|
|
130
|
+
</p>
|
|
131
|
+
</label>
|
|
113
132
|
</>
|
|
114
133
|
)}
|
|
115
134
|
{renderFieldGroup(
|
|
@@ -69,7 +69,7 @@ export function PanelConfigVideoSection({
|
|
|
69
69
|
{renderFieldGroup(
|
|
70
70
|
t("panel.config.groupVideoSource"),
|
|
71
71
|
<>
|
|
72
|
-
<label className="block space-y-1">
|
|
72
|
+
<label className="block space-y-1" data-config-field="videoRemoteUrl">
|
|
73
73
|
<div>{t("panel.config.videoUrl")}</div>
|
|
74
74
|
<Input
|
|
75
75
|
value={selectedElement.videoRemoteUrl ?? ""}
|
|
@@ -79,9 +79,12 @@ export function PanelConfigVideoSection({
|
|
|
79
79
|
videoSrc: e.target.value || selectedElement.videoSrc,
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
|
-
placeholder="
|
|
83
|
-
className="h-7"
|
|
82
|
+
placeholder={t("panel.config.urlScopePlaceholder")}
|
|
83
|
+
className="h-7 font-mono text-[11px]"
|
|
84
84
|
/>
|
|
85
|
+
<p className="text-[10px] text-muted-foreground">
|
|
86
|
+
{t("panel.config.urlScopeHint")}
|
|
87
|
+
</p>
|
|
85
88
|
</label>
|
|
86
89
|
<div className="flex items-center gap-2">
|
|
87
90
|
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -45,19 +60,10 @@ export function evaluateScopeTemplate(
|
|
|
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
|
}
|
|
@@ -148,6 +154,11 @@ export function resolvePanelElementScope(
|
|
|
148
154
|
resolved.chart.labels = labels;
|
|
149
155
|
delete resolved.chart.labelsText;
|
|
150
156
|
}
|
|
157
|
+
const values = materializeChartValuesFromScope(element.chart, scope);
|
|
158
|
+
if (values !== undefined) {
|
|
159
|
+
resolved.chart.values = values;
|
|
160
|
+
delete resolved.chart.valuesText;
|
|
161
|
+
}
|
|
151
162
|
}
|
|
152
163
|
if (resolved.table) {
|
|
153
164
|
const hasDynamicSource =
|