@arronqzy/vue-view 0.1.18 → 0.1.20
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 +6 -6
- package/src/panel/VueViewPanel.vue +51 -3
- package/src/panel/components/PanelConfigSidebar.vue +45 -11
- package/src/panel/components/config/ConfigFieldGroup.vue +27 -3
- package/src/panel/components/config/PanelConfigAudioSection.vue +5 -2
- package/src/panel/components/config/PanelConfigChartSection.vue +9 -2
- package/src/panel/components/config/PanelConfigGeometrySection.vue +2 -0
- package/src/panel/components/config/PanelConfigImageSection.vue +9 -14
- package/src/panel/components/config/PanelConfigMultiSelect.vue +20 -7
- package/src/panel/components/config/PanelConfigNodeInfo.vue +1 -0
- package/src/panel/components/config/PanelConfigReferenceSection.vue +1 -0
- package/src/panel/components/config/PanelConfigScene3dSection.vue +68 -1
- package/src/panel/components/config/PanelConfigStyleSections.vue +9 -5
- package/src/panel/components/config/PanelConfigTextSection.vue +16 -0
- package/src/panel/components/config/PanelConfigVideoSection.vue +4 -2
- package/src/panel/components/config/PanelConfigViewportSection.vue +1 -0
- package/src/panel/components/elementsLayerNodes.ts +2 -1
- 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 +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.20",
|
|
4
4
|
"description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"selecto": "^1.26.3",
|
|
24
24
|
"uuid": "^13.0.0",
|
|
25
25
|
"vue": "^3.5.13",
|
|
26
|
-
"@arronqzy/
|
|
27
|
-
"@arronqzy/i18n": "0.1.
|
|
28
|
-
"@arronqzy/view-scene3d": "0.1.5",
|
|
29
|
-
"@arronqzy/vue-blueprint": "0.1.13",
|
|
26
|
+
"@arronqzy/blueprint-dsl": "1.0.15",
|
|
27
|
+
"@arronqzy/i18n": "0.1.14",
|
|
30
28
|
"@arronqzy/view-table": "0.1.8",
|
|
29
|
+
"@arronqzy/view-scene3d": "0.1.5",
|
|
30
|
+
"@arronqzy/vue-blueprint": "0.1.14",
|
|
31
31
|
"@arronqzy/vue-rx-store": "0.1.6",
|
|
32
|
-
"@arronqzy/
|
|
32
|
+
"@arronqzy/rx-store": "1.0.8"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"vue": "^3.4.0"
|
|
@@ -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";
|
|
@@ -80,10 +83,26 @@ const props = withDefaults(
|
|
|
80
83
|
initialZoom?: number;
|
|
81
84
|
nameSpace?: string | null;
|
|
82
85
|
initialWorkspace?: WorkspaceProjectRecord | null;
|
|
86
|
+
/** 顶栏「设置 → 关于」展示的 npm 包名 */
|
|
87
|
+
packageName?: string | null;
|
|
88
|
+
/** 顶栏「设置 → 关于」展示的包版本号 */
|
|
89
|
+
packageVersion?: string | null;
|
|
83
90
|
}>(),
|
|
84
|
-
{
|
|
91
|
+
{
|
|
92
|
+
initialZoom: 1,
|
|
93
|
+
nameSpace: null,
|
|
94
|
+
initialWorkspace: null,
|
|
95
|
+
packageName: null,
|
|
96
|
+
packageVersion: null,
|
|
97
|
+
}
|
|
85
98
|
);
|
|
86
99
|
|
|
100
|
+
const packageVersionText = computed(() => {
|
|
101
|
+
const version = (props.packageVersion ?? "").trim();
|
|
102
|
+
if (!version) return t("panel.menubar.versionUnavailable");
|
|
103
|
+
return t("panel.menubar.versionValue", { version });
|
|
104
|
+
});
|
|
105
|
+
|
|
87
106
|
const {
|
|
88
107
|
elements,
|
|
89
108
|
allElements,
|
|
@@ -191,6 +210,13 @@ const hasUnlockedSelection = computed(() =>
|
|
|
191
210
|
const viewElementScope = useViewElementScope(
|
|
192
211
|
computed(() => selectedElement.value?.id ?? null)
|
|
193
212
|
);
|
|
213
|
+
const scopeStoreVersion = useViewScopeStoreVersion();
|
|
214
|
+
const scopedCanvasElements = computed(() => {
|
|
215
|
+
void scopeStoreVersion.value;
|
|
216
|
+
return elements.value.map((el) =>
|
|
217
|
+
resolvePanelElementScope(el, getViewElementScope(el.id))
|
|
218
|
+
);
|
|
219
|
+
});
|
|
194
220
|
|
|
195
221
|
function excludeSelectedNode(nodeId: string) {
|
|
196
222
|
const nextIds = selectedIds.value.filter((id) => id !== nodeId);
|
|
@@ -746,7 +772,29 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
|
746
772
|
@delete-project="handleWorkspaceDeleteProject"
|
|
747
773
|
@preview-project="openOnlinePreviewForProject"
|
|
748
774
|
/>
|
|
749
|
-
|
|
775
|
+
<Dropdown :trigger="['click']">
|
|
776
|
+
<Button type="text" class="!text-white">{{ t("panel.menubar.settings") }}</Button>
|
|
777
|
+
<template #overlay>
|
|
778
|
+
<Menu>
|
|
779
|
+
<Menu.ItemGroup :title="t('panel.theme.language')">
|
|
780
|
+
<Menu.Item :class="{ 'ant-menu-item-selected': locale === 'zh-CN' }" @click="setLocale('zh-CN')">{{ t("panel.theme.zhCN") }}</Menu.Item>
|
|
781
|
+
<Menu.Item :class="{ 'ant-menu-item-selected': locale === 'en-US' }" @click="setLocale('en-US')">{{ t("panel.theme.enUS") }}</Menu.Item>
|
|
782
|
+
</Menu.ItemGroup>
|
|
783
|
+
<Menu.Divider />
|
|
784
|
+
<Menu.SubMenu :title="t('panel.menubar.about')" key="about">
|
|
785
|
+
<Menu.Item
|
|
786
|
+
key="version"
|
|
787
|
+
disabled
|
|
788
|
+
class="!cursor-default !opacity-100"
|
|
789
|
+
:data-abuilder-version="props.packageVersion || undefined"
|
|
790
|
+
>
|
|
791
|
+
{{ packageVersionText }}
|
|
792
|
+
</Menu.Item>
|
|
793
|
+
</Menu.SubMenu>
|
|
794
|
+
</Menu>
|
|
795
|
+
</template>
|
|
796
|
+
</Dropdown>
|
|
797
|
+
<Dropdown :trigger="['click']">
|
|
750
798
|
<Button type="text" class="!text-white">{{ t("panel.theme.language") }}</Button>
|
|
751
799
|
<template #overlay>
|
|
752
800
|
<Menu>
|
|
@@ -831,7 +879,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
|
831
879
|
@scroll-change="onViewportScrollChange"
|
|
832
880
|
>
|
|
833
881
|
<ElementsLayer
|
|
834
|
-
:elements="
|
|
882
|
+
:elements="scopedCanvasElements"
|
|
835
883
|
:all-elements="allElements"
|
|
836
884
|
:selected-ids="selectedIds"
|
|
837
885
|
:update-element="updateElement"
|
|
@@ -62,8 +62,10 @@ const isSearchCollapsed = ref(false);
|
|
|
62
62
|
const sidebarScrollRef = ref<HTMLElement | null>(null);
|
|
63
63
|
const configScrollPosById = new Map<string, number>();
|
|
64
64
|
const activeConfigScrollKey = ref<string | null>(null);
|
|
65
|
+
const expandedSectionsById = new Map<string, Record<string, boolean>>();
|
|
66
|
+
const activeExpandedKey = ref<string | null>(null);
|
|
65
67
|
|
|
66
|
-
const
|
|
68
|
+
const DEFAULT_EXPANDED_SECTIONS: Record<string, boolean> = {
|
|
67
69
|
nodeInfo: true,
|
|
68
70
|
styleBackground: false,
|
|
69
71
|
styleBorder: false,
|
|
@@ -78,6 +80,10 @@ const expandedSections = ref<Record<string, boolean>>({
|
|
|
78
80
|
reference: true,
|
|
79
81
|
imageConfig: true,
|
|
80
82
|
tableConfig: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const expandedSections = ref<Record<string, boolean>>({
|
|
86
|
+
...DEFAULT_EXPANDED_SECTIONS,
|
|
81
87
|
});
|
|
82
88
|
|
|
83
89
|
const normalizedSearch = computed(() => configSearch.value.trim().toLowerCase());
|
|
@@ -118,12 +124,39 @@ watch(
|
|
|
118
124
|
}
|
|
119
125
|
activeConfigScrollKey.value = nextKey;
|
|
120
126
|
void nextTick(() => restoreConfigScroll(nextKey));
|
|
127
|
+
|
|
128
|
+
if (prevKey && prevKey !== nextKey) {
|
|
129
|
+
expandedSectionsById.set(prevKey, { ...expandedSections.value });
|
|
130
|
+
}
|
|
131
|
+
activeExpandedKey.value = nextKey;
|
|
132
|
+
if (!nextKey) {
|
|
133
|
+
expandedSections.value = { ...DEFAULT_EXPANDED_SECTIONS };
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const saved = expandedSectionsById.get(nextKey);
|
|
137
|
+
if (saved) {
|
|
138
|
+
expandedSections.value = { ...saved };
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const initial = { ...DEFAULT_EXPANDED_SECTIONS };
|
|
142
|
+
expandedSectionsById.set(nextKey, initial);
|
|
143
|
+
expandedSections.value = initial;
|
|
121
144
|
},
|
|
122
145
|
{ flush: "post" }
|
|
123
146
|
);
|
|
124
147
|
|
|
125
148
|
onMounted(() => {
|
|
126
149
|
activeConfigScrollKey.value = configScrollKey.value;
|
|
150
|
+
activeExpandedKey.value = configScrollKey.value;
|
|
151
|
+
if (configScrollKey.value) {
|
|
152
|
+
const saved = expandedSectionsById.get(configScrollKey.value);
|
|
153
|
+
if (saved) expandedSections.value = { ...saved };
|
|
154
|
+
else {
|
|
155
|
+
const initial = { ...DEFAULT_EXPANDED_SECTIONS };
|
|
156
|
+
expandedSectionsById.set(configScrollKey.value, initial);
|
|
157
|
+
expandedSections.value = initial;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
127
160
|
const el = sidebarScrollRef.value;
|
|
128
161
|
el?.addEventListener("scroll", onConfigSidebarScroll, { passive: true });
|
|
129
162
|
restoreConfigScroll(configScrollKey.value);
|
|
@@ -171,13 +204,23 @@ const materialType = computed(() => props.selectedElement?.materialType ?? "");
|
|
|
171
204
|
|
|
172
205
|
const forceOpenSections = computed(() => hasSearch.value);
|
|
173
206
|
provide("configHasSearch", hasSearch);
|
|
207
|
+
provide("configExpandState", {
|
|
208
|
+
isExpanded: (key: string, defaultValue = true) =>
|
|
209
|
+
expandedSections.value[key] ?? defaultValue,
|
|
210
|
+
setExpanded: (key: string, next: boolean) => {
|
|
211
|
+
setSectionExpanded(key, next);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
174
214
|
|
|
175
215
|
function isSectionExpanded(key: string, defaultValue = true) {
|
|
176
216
|
return expandedSections.value[key] ?? defaultValue;
|
|
177
217
|
}
|
|
178
218
|
|
|
179
219
|
function setSectionExpanded(key: string, next: boolean) {
|
|
180
|
-
|
|
220
|
+
const updated = { ...expandedSections.value, [key]: next };
|
|
221
|
+
expandedSections.value = updated;
|
|
222
|
+
const id = activeExpandedKey.value;
|
|
223
|
+
if (id) expandedSectionsById.set(id, updated);
|
|
181
224
|
}
|
|
182
225
|
|
|
183
226
|
function shouldShowSection(key: string, title: string, searchTerms: string[] = []) {
|
|
@@ -223,15 +266,6 @@ onMounted(() => {
|
|
|
223
266
|
watch(isSearchCollapsed, (next) => {
|
|
224
267
|
window.localStorage.setItem(SEARCH_COLLAPSE_STORAGE_KEY, next ? "1" : "0");
|
|
225
268
|
});
|
|
226
|
-
|
|
227
|
-
watch(
|
|
228
|
-
() => [props.selectedElement?.id, props.selectedElement?.materialType],
|
|
229
|
-
() => {
|
|
230
|
-
if (isChartElement.value) {
|
|
231
|
-
setSectionExpanded("chartBasic", true);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
);
|
|
235
269
|
</script>
|
|
236
270
|
|
|
237
271
|
<template>
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
import { computed, inject, ref, type ComputedRef } from "vue";
|
|
3
3
|
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
4
4
|
|
|
5
|
+
export type ConfigExpandState = {
|
|
6
|
+
isExpanded: (key: string, defaultValue?: boolean) => boolean;
|
|
7
|
+
setExpanded: (key: string, next: boolean) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
const props = withDefaults(
|
|
6
11
|
defineProps<{
|
|
7
12
|
title: string;
|
|
8
13
|
hint?: boolean;
|
|
9
14
|
collapsible?: boolean;
|
|
10
15
|
defaultOpen?: boolean;
|
|
16
|
+
/** Stable key for remembering expand state per view node */
|
|
17
|
+
groupKey?: string;
|
|
11
18
|
}>(),
|
|
12
19
|
{ defaultOpen: true, collapsible: false }
|
|
13
20
|
);
|
|
@@ -16,13 +23,30 @@ const hasSearch = inject<ComputedRef<boolean>>(
|
|
|
16
23
|
"configHasSearch",
|
|
17
24
|
computed(() => false)
|
|
18
25
|
);
|
|
26
|
+
const expandState = inject<ConfigExpandState | null>("configExpandState", null);
|
|
27
|
+
|
|
28
|
+
const localOpen = ref(props.defaultOpen);
|
|
29
|
+
const storageKey = computed(
|
|
30
|
+
() => props.groupKey?.trim() || `fg:${props.title}`
|
|
31
|
+
);
|
|
19
32
|
|
|
20
|
-
const
|
|
21
|
-
|
|
33
|
+
const isOpen = computed(() => {
|
|
34
|
+
if (!props.collapsible) return true;
|
|
35
|
+
if (hasSearch.value) return true;
|
|
36
|
+
if (expandState) {
|
|
37
|
+
return expandState.isExpanded(storageKey.value, props.defaultOpen);
|
|
38
|
+
}
|
|
39
|
+
return localOpen.value;
|
|
40
|
+
});
|
|
22
41
|
|
|
23
42
|
function toggle() {
|
|
24
43
|
if (!props.collapsible || hasSearch.value) return;
|
|
25
|
-
|
|
44
|
+
const next = !isOpen.value;
|
|
45
|
+
if (expandState) {
|
|
46
|
+
expandState.setExpanded(storageKey.value, next);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
localOpen.value = next;
|
|
26
50
|
}
|
|
27
51
|
</script>
|
|
28
52
|
|
|
@@ -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
|
|
@@ -184,6 +186,7 @@ function onPosterFileChange(e: Event) {
|
|
|
184
186
|
</ConfigFieldGroup>
|
|
185
187
|
<ConfigFieldGroup
|
|
186
188
|
:title="t('panel.config.groupDisplayStyle')"
|
|
189
|
+
group-key="audioDisplayStyle"
|
|
187
190
|
collapsible
|
|
188
191
|
:default-open="false"
|
|
189
192
|
>
|
|
@@ -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
|
|
|
@@ -234,6 +238,7 @@ function onOptionJsonChange(v: string) {
|
|
|
234
238
|
</ConfigFieldGroup>
|
|
235
239
|
<ConfigFieldGroup
|
|
236
240
|
:title="t('panel.config.groupChartDisplayMore')"
|
|
241
|
+
group-key="chartDisplayMore"
|
|
237
242
|
collapsible
|
|
238
243
|
:default-open="false"
|
|
239
244
|
>
|
|
@@ -304,6 +309,7 @@ function onOptionJsonChange(v: string) {
|
|
|
304
309
|
|
|
305
310
|
<ConfigFieldGroup
|
|
306
311
|
:title="t('panel.config.groupTooltip')"
|
|
312
|
+
group-key="chartTooltip"
|
|
307
313
|
collapsible
|
|
308
314
|
:default-open="false"
|
|
309
315
|
>
|
|
@@ -365,6 +371,7 @@ function onOptionJsonChange(v: string) {
|
|
|
365
371
|
<ConfigFieldGroup
|
|
366
372
|
v-if="['bar', 'line', 'area', 'scatter'].includes(selectedChartType)"
|
|
367
373
|
:title="t('panel.config.groupAxes')"
|
|
374
|
+
group-key="chartAxes"
|
|
368
375
|
collapsible
|
|
369
376
|
:default-open="false"
|
|
370
377
|
>
|
|
@@ -140,6 +140,7 @@ function clearSketch() {
|
|
|
140
140
|
</ConfigFieldGroup>
|
|
141
141
|
<ConfigFieldGroup
|
|
142
142
|
:title="t('panel.config.groupCanvasScript')"
|
|
143
|
+
group-key="geometryCanvasScript"
|
|
143
144
|
collapsible
|
|
144
145
|
:default-open="false"
|
|
145
146
|
>
|
|
@@ -158,6 +159,7 @@ function clearSketch() {
|
|
|
158
159
|
</ConfigFieldGroup>
|
|
159
160
|
<ConfigFieldGroup
|
|
160
161
|
:title="t('panel.config.groupSketchOverlay')"
|
|
162
|
+
group-key="geometrySketch"
|
|
161
163
|
collapsible
|
|
162
164
|
:default-open="false"
|
|
163
165
|
>
|
|
@@ -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
|
|
@@ -106,6 +100,7 @@ function onFileChange(e: Event) {
|
|
|
106
100
|
</ConfigFieldGroup>
|
|
107
101
|
<ConfigFieldGroup
|
|
108
102
|
:title="t('panel.config.groupDisplayMode')"
|
|
103
|
+
group-key="imageDisplayMode"
|
|
109
104
|
collapsible
|
|
110
105
|
:default-open="false"
|
|
111
106
|
>
|
|
@@ -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";
|
|
@@ -83,18 +84,21 @@ function onFileChange(e: Event) {
|
|
|
83
84
|
</ConfigFieldGroup>
|
|
84
85
|
<ConfigFieldGroup
|
|
85
86
|
:title="t('panel.config.groupBgLayout')"
|
|
87
|
+
group-key="styleBgLayout"
|
|
86
88
|
collapsible
|
|
87
89
|
:default-open="false"
|
|
88
90
|
>
|
|
89
|
-
<label class="block space-y-1">
|
|
90
|
-
<div>{{ t("panel.
|
|
91
|
+
<label class="block space-y-1" data-config-field="style.backgroundImage">
|
|
92
|
+
<div>{{ t("panel.config.backgroundImage") }}</div>
|
|
91
93
|
<Input
|
|
92
94
|
size="small"
|
|
93
|
-
|
|
95
|
+
class="font-mono text-[11px]"
|
|
96
|
+
:value="element.style?.backgroundImageRemoteUrl ?? element.style?.backgroundImage ?? ''"
|
|
94
97
|
:disabled="!isEditable"
|
|
95
|
-
placeholder=
|
|
96
|
-
@update:value="(v: string) => patchStyle(
|
|
98
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
99
|
+
@update:value="(v: string) => patchStyle(patchBackgroundImageFromInput(String(v ?? '')))"
|
|
97
100
|
/>
|
|
101
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
98
102
|
</label>
|
|
99
103
|
<div class="flex items-center gap-2">
|
|
100
104
|
<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">
|
|
@@ -200,6 +215,7 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
|
|
|
200
215
|
</ConfigFieldGroup>
|
|
201
216
|
<ConfigFieldGroup
|
|
202
217
|
:title="t('panel.config.groupInputAbility')"
|
|
218
|
+
group-key="textInputAbility"
|
|
203
219
|
collapsible
|
|
204
220
|
:default-open="false"
|
|
205
221
|
>
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -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 =
|