@arronqzy/vue-view 0.1.20 → 0.1.22
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/README.md +25 -21
- package/package.json +6 -6
- package/src/index.ts +2 -0
- package/src/panel/VueViewOnlinePreview.vue +6 -1
- package/src/panel/VueViewPanel.vue +88 -4
- package/src/panel/components/ConfigHintIcon.vue +15 -2
- package/src/panel/components/config/PanelConfigAudioSection.vue +7 -4
- package/src/panel/components/config/PanelConfigChartSection.vue +14 -8
- package/src/panel/components/config/PanelConfigImageSection.vue +2 -2
- package/src/panel/components/config/PanelConfigMultiSelect.vue +7 -2
- package/src/panel/components/config/PanelConfigScene3dSection.vue +8 -4
- package/src/panel/components/config/PanelConfigStyleSections.vue +8 -4
- package/src/panel/components/config/PanelConfigTextSection.vue +8 -4
- package/src/panel/components/config/PanelConfigVideoSection.vue +8 -4
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +6 -9
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +1 -0
- package/src/panel/components/table/ConditionEditor.vue +7 -2
- package/src/panel/components/table/PanelConfigTableSection.vue +10 -4
- package/src/panel/hooks/usePanelElements.ts +18 -2
- package/src/panel/library/event-subscription.ts +20 -0
- package/src/panel/utils/panelElementDefaults.ts +47 -1
- package/src/panel/utils/scope-template.ts +20 -8
package/README.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# @arronqzy/vue-view
|
|
2
2
|
|
|
3
|
-
Vue 3
|
|
3
|
+
Vue 3 视图编辑器,功能对齐 `@arronqzy/react-view`,组件用 Ant Design Vue。大多数 Vue 宿主应直接用 [`@arronqzy/abuilder-vue`](../abuilder-vue/README.md)。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
7
|
+
## 能力
|
|
8
|
+
|
|
9
|
+
- 无限平移 / 缩放(Infinite Viewer)、Moveable、Selecto 框选
|
|
10
|
+
- 图表、文本、表格、图片、音视频、几何、网格、三维、引用节点
|
|
11
|
+
- 多图层、映射图层、主图层、锁定与合并
|
|
12
|
+
- 按物料类型的配置侧栏,支持 Scope 模版
|
|
13
|
+
- IndexedDB 多项目、导入导出、跨标签同步
|
|
14
|
+
- 分屏蓝图(`@arronqzy/vue-blueprint`)和调试时的 Scope 回写
|
|
15
|
+
- `VueViewOnlinePreview` 只读预览
|
|
16
|
+
|
|
17
|
+
打开蓝图后的分屏和 React 版一样:上面视图,下面节点:
|
|
18
|
+
|
|
19
|
+

|
|
14
20
|
|
|
15
21
|
## 使用
|
|
16
22
|
|
|
@@ -29,7 +35,7 @@ createApp(VueViewPanel).use(Antd).mount("#app");
|
|
|
29
35
|
import { VueViewOnlinePreview, parseOnlinePreviewSearchParams } from "@arronqzy/vue-view";
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
URL
|
|
38
|
+
URL:`?preview=online&projectId=<id>&pid=<instanceId>`。传入 `workspace` 时优先于 `projectId`。
|
|
33
39
|
|
|
34
40
|
### 宿主集成
|
|
35
41
|
|
|
@@ -43,21 +49,19 @@ import {
|
|
|
43
49
|
} from "@arronqzy/vue-view";
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
创建 /
|
|
52
|
+
创建 / 同步发出 `workspace:add` / `workspace:sync`。`parseWorkspaceData` 的 `value` 可作 `initialWorkspace`。
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
IndexedDB CRUD 与 `getPreviewSnapshot` **不导出**。缩略图只在 React 包实现。
|
|
55
|
+
|
|
56
|
+
## 和 React 版的差别
|
|
49
57
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
- `@arronqzy/
|
|
53
|
-
- `@arronqzy/blueprint-dsl` — 蓝图 DSL
|
|
54
|
-
- `ant-design-vue`、`echarts`、`moveable`、`selecto`、`infinite-viewer`
|
|
58
|
+
- UI 是 Ant Design Vue
|
|
59
|
+
- 标尺是 canvas 实现(可换成 `@scena/ruler`)
|
|
60
|
+
- 画布状态仍是共享的 `@arronqzy/rx-store`
|
|
55
61
|
|
|
56
|
-
##
|
|
62
|
+
## 依赖
|
|
57
63
|
|
|
58
|
-
-
|
|
59
|
-
- 标尺使用 canvas 兼容实现(可替换为 `@scena/ruler`)
|
|
60
|
-
- 共享 `@arronqzy/rx-store` 与 `panel/utils` 数据逻辑
|
|
64
|
+
`@arronqzy/rx-store`、`@arronqzy/vue-rx-store`、`@arronqzy/vue-blueprint`、`@arronqzy/blueprint-dsl`、`ant-design-vue`、`echarts`、`moveable`、`selecto`、`infinite-viewer`。
|
|
61
65
|
|
|
62
66
|
## 许可证
|
|
63
67
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/vue-view",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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/blueprint-dsl": "1.0.
|
|
27
|
-
"@arronqzy/i18n": "0.1.
|
|
26
|
+
"@arronqzy/blueprint-dsl": "1.0.17",
|
|
27
|
+
"@arronqzy/i18n": "0.1.17",
|
|
28
28
|
"@arronqzy/view-table": "0.1.8",
|
|
29
|
-
"@arronqzy/
|
|
30
|
-
"@arronqzy/vue-blueprint": "0.1.
|
|
29
|
+
"@arronqzy/rx-store": "1.0.8",
|
|
30
|
+
"@arronqzy/vue-blueprint": "0.1.17",
|
|
31
31
|
"@arronqzy/vue-rx-store": "0.1.6",
|
|
32
|
-
"@arronqzy/
|
|
32
|
+
"@arronqzy/view-scene3d": "0.1.5"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"vue": "^3.4.0"
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type {
|
|
|
22
22
|
AbuilderEventPayloadMap,
|
|
23
23
|
WorkspaceAddEventPayload,
|
|
24
24
|
WorkspaceSyncEventPayload,
|
|
25
|
+
ApiCollectionUploadStartEventPayload,
|
|
26
|
+
ApiCollectionUploadSuccessEventPayload,
|
|
25
27
|
WorkspaceData,
|
|
26
28
|
} from "./panel/library/event-subscription";
|
|
27
29
|
export type {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
3
|
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
4
4
|
import {
|
|
5
|
+
useBlueprintDslLocaleSync,
|
|
5
6
|
BlueprintGraph,
|
|
6
7
|
documentToRunnableGraph,
|
|
7
8
|
getBlueprintLibraryRecord,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
useViewScopeStoreVersion,
|
|
23
24
|
} from "./scope/view-scope-store";
|
|
24
25
|
import { resolvePanelElementScope } from "./utils/scope-template";
|
|
26
|
+
import { localizePanelElement } from "./utils/panelElementDefaults";
|
|
25
27
|
import {
|
|
26
28
|
getWorkspaceProject,
|
|
27
29
|
type WorkspaceProjectRecord,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
import { applyPreviewSceneFill, readOutputScale } from "./utils/outputScale";
|
|
41
43
|
|
|
42
44
|
const { t, locale } = useI18n();
|
|
45
|
+
useBlueprintDslLocaleSync();
|
|
43
46
|
|
|
44
47
|
const PREVIEW_BOOT_PHASES: PageLifecyclePhase[] = ["mounted"];
|
|
45
48
|
|
|
@@ -194,7 +197,9 @@ const scopedLayerElements = computed(() => {
|
|
|
194
197
|
const sceneBounds = computed(() => computePanelSceneBounds(scopedLayerElements.value));
|
|
195
198
|
|
|
196
199
|
const displayElements = computed(() =>
|
|
197
|
-
scopedLayerElements.value.map((
|
|
200
|
+
scopedLayerElements.value.map((source) => {
|
|
201
|
+
void locale.value;
|
|
202
|
+
const el = localizePanelElement(source, t);
|
|
198
203
|
const x = el.x - sceneBounds.value.minX;
|
|
199
204
|
const y = el.y - sceneBounds.value.minY;
|
|
200
205
|
if (outputScale.value) return { ...el, x, y };
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
|
|
3
3
|
import type { State } from "@arronqzy/rx-store";
|
|
4
4
|
import {
|
|
5
|
+
useBlueprintDslLocaleSync,
|
|
6
|
+
ApiCollectionManagerDialog,
|
|
5
7
|
BlueprintGraph,
|
|
6
8
|
abortClockNode,
|
|
7
9
|
blueprintDocumentsEqual,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
documentToRunnableGraph,
|
|
13
15
|
downloadBlueprintExport,
|
|
14
16
|
getBlueprintLibraryRecord,
|
|
17
|
+
hydrateApiCollections,
|
|
15
18
|
libraryRecordFromImport,
|
|
16
19
|
listBlueprintLibrary,
|
|
17
20
|
parseBlueprintImportFile,
|
|
@@ -27,7 +30,7 @@ import {
|
|
|
27
30
|
resolveRunnableNodeType,
|
|
28
31
|
resolveViewElementIds,
|
|
29
32
|
} from "@arronqzy/vue-blueprint";
|
|
30
|
-
import { collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE, type LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
|
|
33
|
+
import { collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE, buildApiCollectionSyncPatches, toApiCollectionSyncResult, type LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
|
|
31
34
|
import {
|
|
32
35
|
Button,
|
|
33
36
|
Checkbox,
|
|
@@ -49,6 +52,7 @@ import {
|
|
|
49
52
|
useWorkspaceProjects,
|
|
50
53
|
} from "./hooks/useWorkspaceProjects";
|
|
51
54
|
import type { WorkspaceProjectRecord } from "./library/workspace-project-db";
|
|
55
|
+
import { AbuilderEvents, emitAbuilderEvent } from "./library/event-subscription";
|
|
52
56
|
import { type ViewportZoom } from "./viewportZoom";
|
|
53
57
|
import PanelCanvas from "./components/PanelCanvas.vue";
|
|
54
58
|
import ElementsLayer from "./components/ElementsLayer.vue";
|
|
@@ -69,12 +73,14 @@ import {
|
|
|
69
73
|
import { resolvePanelElementScope } from "./utils/scope-template";
|
|
70
74
|
import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
|
|
71
75
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
76
|
+
import { relocalizeMessage } from "@arronqzy/i18n";
|
|
72
77
|
import { getPanelMessages } from "./constants/messages";
|
|
73
78
|
import BusyOverlay from "./components/BusyOverlay.vue";
|
|
74
79
|
import { assertFileSize, parseJsonText, runBusyTask } from "./utils/async-work";
|
|
75
80
|
import "../tailwind.css";
|
|
76
81
|
|
|
77
82
|
const { t, locale, setLocale } = useI18n();
|
|
83
|
+
useBlueprintDslLocaleSync();
|
|
78
84
|
const panelMessages = () => getPanelMessages(t);
|
|
79
85
|
|
|
80
86
|
const props = withDefaults(
|
|
@@ -149,6 +155,18 @@ const workspaceBlueprintRef = shallowRef<{
|
|
|
149
155
|
} | null>(null);
|
|
150
156
|
const configFocus = ref<WorkspaceConfigFocus>("view");
|
|
151
157
|
const productName = ref(t("panel.defaults.unnamedProduct"));
|
|
158
|
+
const displayProductName = computed(() => {
|
|
159
|
+
void locale.value;
|
|
160
|
+
return relocalizeMessage(productName.value, "panel.defaults.unnamedProduct", t);
|
|
161
|
+
});
|
|
162
|
+
const displayBlueprintName = computed(() => {
|
|
163
|
+
void locale.value;
|
|
164
|
+
return relocalizeMessage(
|
|
165
|
+
blueprintMeta.value.name,
|
|
166
|
+
["panel.defaults.unnamedBlueprint", "blueprint.dialog.unnamedBlueprint"],
|
|
167
|
+
t
|
|
168
|
+
);
|
|
169
|
+
});
|
|
152
170
|
const titleIconDataUrl = ref("");
|
|
153
171
|
const editingLayerId = ref<string | null>(null);
|
|
154
172
|
const editingLayerName = ref("");
|
|
@@ -158,6 +176,7 @@ const leftWidth = ref(240);
|
|
|
158
176
|
const rightWidth = ref(300);
|
|
159
177
|
const importInputRef = ref<HTMLInputElement | null>(null);
|
|
160
178
|
const blueprintImportInputRef = ref<HTMLInputElement | null>(null);
|
|
179
|
+
const apiCollectionManagerOpen = ref(false);
|
|
161
180
|
|
|
162
181
|
const viewportEl = ref<HTMLDivElement | null>(null);
|
|
163
182
|
const canvasEl = ref<HTMLDivElement | null>(null);
|
|
@@ -342,7 +361,7 @@ function handleViewScopeUpdate(viewElementIds: string[], scope: unknown) {
|
|
|
342
361
|
const blueprintDebugSession = useBlueprintDebugSession({
|
|
343
362
|
graph: blueprintGraph,
|
|
344
363
|
blueprintId: activeBlueprintLibraryId,
|
|
345
|
-
blueprintName: computed(() =>
|
|
364
|
+
blueprintName: computed(() => displayBlueprintName.value || t("panel.defaults.unnamedBlueprint")),
|
|
346
365
|
resolveLibraryBlueprint,
|
|
347
366
|
libraryNameById: blueprintLibraryNameById,
|
|
348
367
|
onExecutionBlocked: handleBlueprintExecutionBlocked,
|
|
@@ -391,6 +410,16 @@ async function refreshBlueprintLibrary() {
|
|
|
391
410
|
blueprintLibraryItems.value = await listBlueprintLibrary(props.nameSpace);
|
|
392
411
|
}
|
|
393
412
|
|
|
413
|
+
watch(
|
|
414
|
+
() => props.nameSpace,
|
|
415
|
+
(nameSpace) => {
|
|
416
|
+
void hydrateApiCollections(nameSpace).catch((error) => {
|
|
417
|
+
console.error("[abuilder] hydrate api collections failed", error);
|
|
418
|
+
});
|
|
419
|
+
},
|
|
420
|
+
{ immediate: true }
|
|
421
|
+
);
|
|
422
|
+
|
|
394
423
|
onMounted(() => {
|
|
395
424
|
void refreshBlueprintLibrary();
|
|
396
425
|
});
|
|
@@ -604,6 +633,50 @@ function openBlueprintExport() {
|
|
|
604
633
|
});
|
|
605
634
|
}
|
|
606
635
|
|
|
636
|
+
function handleApiCollectionUploadStart(info: {
|
|
637
|
+
fileNames: string[];
|
|
638
|
+
count: number;
|
|
639
|
+
}) {
|
|
640
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionUploadStart, info);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function handleApiCollectionUploadSuccess(info: {
|
|
644
|
+
collections: Array<{
|
|
645
|
+
id: string;
|
|
646
|
+
name: string;
|
|
647
|
+
apiCount: number;
|
|
648
|
+
persistLocal: boolean;
|
|
649
|
+
}>;
|
|
650
|
+
}) {
|
|
651
|
+
emitAbuilderEvent(AbuilderEvents.apiCollectionUploadSuccess, info);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function handleApiCollectionSync(record: {
|
|
655
|
+
id: string;
|
|
656
|
+
document: import("@arronqzy/blueprint-dsl").ApiCollectionDocument;
|
|
657
|
+
}) {
|
|
658
|
+
const patches = buildApiCollectionSyncPatches({
|
|
659
|
+
collectionId: record.id,
|
|
660
|
+
document: record.document,
|
|
661
|
+
nodes: blueprintGraph.value.document.nodes
|
|
662
|
+
.filter((node) => node.role === "fetch" || Boolean(node.fetchConfig))
|
|
663
|
+
.map((node) => ({
|
|
664
|
+
id: node.id,
|
|
665
|
+
label: node.label,
|
|
666
|
+
fetchConfig: node.fetchConfig,
|
|
667
|
+
})),
|
|
668
|
+
});
|
|
669
|
+
if (patches.length === 0) return toApiCollectionSyncResult([]);
|
|
670
|
+
let next = blueprintGraph.value;
|
|
671
|
+
for (const patch of patches) {
|
|
672
|
+
next = next.updateNode(patch.nodeId, {
|
|
673
|
+
fetchConfig: patch.fetchConfigPatch as import("@arronqzy/blueprint-dsl").FetchRequestConfig,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
blueprintGraph.value = next;
|
|
677
|
+
return toApiCollectionSyncResult(patches);
|
|
678
|
+
}
|
|
679
|
+
|
|
607
680
|
async function handleBlueprintImportFile(file: File) {
|
|
608
681
|
try {
|
|
609
682
|
await runBusyTask(t("common.importingBlueprint"), async () => {
|
|
@@ -757,6 +830,8 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
|
757
830
|
<Menu>
|
|
758
831
|
<Menu.Item @click="openBlueprintExport">{{ t("panel.menubar.exportBlueprint") }}</Menu.Item>
|
|
759
832
|
<Menu.Item @click="blueprintImportInputRef?.click()">{{ t("panel.menubar.importBlueprint") }}</Menu.Item>
|
|
833
|
+
<Menu.Divider />
|
|
834
|
+
<Menu.Item @click="apiCollectionManagerOpen = true">{{ t("panel.menubar.apiCollections") }}</Menu.Item>
|
|
760
835
|
</Menu>
|
|
761
836
|
</template>
|
|
762
837
|
</Dropdown>
|
|
@@ -832,8 +907,9 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
|
832
907
|
<div class="flex h-full min-h-0 flex-col">
|
|
833
908
|
<div class="flex flex-wrap items-center gap-2 border-b bg-white px-3 py-2">
|
|
834
909
|
<Input
|
|
835
|
-
|
|
910
|
+
:value="displayProductName"
|
|
836
911
|
size="small"
|
|
912
|
+
@update:value="(value: string) => (productName = value)"
|
|
837
913
|
class="w-[200px]"
|
|
838
914
|
:placeholder="t('panel.menubar.productName')"
|
|
839
915
|
/>
|
|
@@ -859,7 +935,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
|
859
935
|
:blueprint-props="blueprintCanvasProps"
|
|
860
936
|
:blueprint-library-items="blueprintLibraryItems"
|
|
861
937
|
:active-blueprint-library-id="activeBlueprintLibraryId"
|
|
862
|
-
:current-blueprint-label="
|
|
938
|
+
:current-blueprint-label="displayBlueprintName"
|
|
863
939
|
:can-sync-blueprint="blueprintLibraryDirty"
|
|
864
940
|
:blueprint-debug="blueprintDebugToolbar"
|
|
865
941
|
:on-select-blueprint-library-item="(id) => void handleSelectBlueprintLibraryItem(id)"
|
|
@@ -1021,6 +1097,14 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
|
1021
1097
|
<Input v-model:value="mergeLayerName" :placeholder="t('panel.layers.mergedNamePlaceholderOptional')" />
|
|
1022
1098
|
<div class="mt-2 text-xs text-gray-500">{{ t("panel.layers.mergeSelectedCount", { count: mergeSelectedCount }) }}</div>
|
|
1023
1099
|
</Modal>
|
|
1100
|
+
|
|
1101
|
+
<ApiCollectionManagerDialog
|
|
1102
|
+
:open="apiCollectionManagerOpen"
|
|
1103
|
+
:on-open-change="(open) => (apiCollectionManagerOpen = open)"
|
|
1104
|
+
:on-upload-start="handleApiCollectionUploadStart"
|
|
1105
|
+
:on-upload-success="handleApiCollectionUploadSuccess"
|
|
1106
|
+
:on-sync-collection="handleApiCollectionSync"
|
|
1107
|
+
/>
|
|
1024
1108
|
</Layout>
|
|
1025
1109
|
</template>
|
|
1026
1110
|
|
|
@@ -26,11 +26,24 @@ const resolvedLabel = computed(() => props.label || t("common.hint"));
|
|
|
26
26
|
type="button"
|
|
27
27
|
:class="
|
|
28
28
|
buttonClass ??
|
|
29
|
-
'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full
|
|
29
|
+
'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
|
30
30
|
"
|
|
31
31
|
:aria-label="t('common.hintAria', { label: resolvedLabel })"
|
|
32
32
|
>
|
|
33
|
-
|
|
33
|
+
<svg
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
fill="none"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
stroke-width="2"
|
|
38
|
+
stroke-linecap="round"
|
|
39
|
+
stroke-linejoin="round"
|
|
40
|
+
class="h-3.5 w-3.5"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
>
|
|
43
|
+
<circle cx="12" cy="12" r="10" />
|
|
44
|
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
|
45
|
+
<path d="M12 17h.01" />
|
|
46
|
+
</svg>
|
|
34
47
|
</button>
|
|
35
48
|
</Tooltip>
|
|
36
49
|
</template>
|
|
@@ -141,7 +141,12 @@ function onPosterFileChange(e: Event) {
|
|
|
141
141
|
>
|
|
142
142
|
<ConfigFieldGroup :title="t('panel.config.groupAudioSource')">
|
|
143
143
|
<label class="block space-y-1" data-config-field="audioRemoteUrl">
|
|
144
|
-
<div
|
|
144
|
+
<div class="flex items-center gap-1">
|
|
145
|
+
{{ t("panel.config.audioUrl") }}
|
|
146
|
+
<ConfigHintIcon :label="t('panel.config.audioUrl')">
|
|
147
|
+
{{ t("panel.config.urlScopeHint") }}
|
|
148
|
+
</ConfigHintIcon>
|
|
149
|
+
</div>
|
|
145
150
|
<Input
|
|
146
151
|
size="small"
|
|
147
152
|
class="font-mono text-[11px]"
|
|
@@ -152,9 +157,7 @@ function onPosterFileChange(e: Event) {
|
|
|
152
157
|
audioRemoteUrl: v || undefined,
|
|
153
158
|
audioSrc: v || element.audioSrc,
|
|
154
159
|
})"
|
|
155
|
-
/>
|
|
156
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
157
|
-
</label>
|
|
160
|
+
/> </label>
|
|
158
161
|
<div class="flex items-center gap-2">
|
|
159
162
|
<label
|
|
160
163
|
class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
|
|
@@ -146,27 +146,33 @@ function onOptionJsonChange(v: string) {
|
|
|
146
146
|
</ConfigFieldGroup>
|
|
147
147
|
<ConfigFieldGroup :title="t('panel.config.groupData')">
|
|
148
148
|
<label class="block space-y-1" data-config-field="chart.labelsText">
|
|
149
|
-
<div
|
|
149
|
+
<div class="flex items-center gap-1">
|
|
150
|
+
{{ t("panel.config.labelsCsv") }}
|
|
151
|
+
<ConfigHintIcon :label="t('panel.config.labelsCsv')">
|
|
152
|
+
{{ t("panel.config.scopeTemplateHint") }}
|
|
153
|
+
</ConfigHintIcon>
|
|
154
|
+
</div>
|
|
150
155
|
<Input
|
|
151
156
|
size="small"
|
|
152
157
|
:value="getChartLabelsDisplayText(element.chart)"
|
|
153
158
|
:disabled="!isEditable"
|
|
154
159
|
:placeholder="t('panel.config.labelsCsvPlaceholder')"
|
|
155
160
|
@update:value="(v: string) => updateChart({ labelsText: v })"
|
|
156
|
-
/>
|
|
157
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
|
|
158
|
-
</label>
|
|
161
|
+
/> </label>
|
|
159
162
|
<label class="block space-y-1" data-config-field="chart.valuesText">
|
|
160
|
-
<div
|
|
163
|
+
<div class="flex items-center gap-1">
|
|
164
|
+
{{ t("panel.config.valuesCsv") }}
|
|
165
|
+
<ConfigHintIcon :label="t('panel.config.valuesCsv')">
|
|
166
|
+
{{ t("panel.config.scopeTemplateHint") }}
|
|
167
|
+
</ConfigHintIcon>
|
|
168
|
+
</div>
|
|
161
169
|
<Input
|
|
162
170
|
size="small"
|
|
163
171
|
:value="getChartValuesDisplayText(element.chart)"
|
|
164
172
|
:disabled="!isEditable"
|
|
165
173
|
:placeholder="t('panel.config.valuesCsvPlaceholder')"
|
|
166
174
|
@update:value="(v: string) => updateChart({ valuesText: v })"
|
|
167
|
-
/>
|
|
168
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
|
|
169
|
-
</label>
|
|
175
|
+
/> </label>
|
|
170
176
|
</ConfigFieldGroup>
|
|
171
177
|
|
|
172
178
|
<ConfigFieldGroup
|
|
@@ -74,7 +74,8 @@ function onFileChange(e: Event) {
|
|
|
74
74
|
<div class="flex items-center gap-1">
|
|
75
75
|
<span>{{ t("panel.config.imageUrlCss") }}</span>
|
|
76
76
|
<ConfigHintIcon :label="t('panel.config.imageUrlHintLabel')">
|
|
77
|
-
{{ t("panel.config.imageUrlHint") }}
|
|
77
|
+
<p>{{ t("panel.config.imageUrlHint") }}</p>
|
|
78
|
+
<p class="mt-1">{{ t("panel.config.urlScopeHint") }}</p>
|
|
78
79
|
</ConfigHintIcon>
|
|
79
80
|
</div>
|
|
80
81
|
<Input
|
|
@@ -85,7 +86,6 @@ function onFileChange(e: Event) {
|
|
|
85
86
|
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
86
87
|
@update:value="(v: string) => patchStyle(patchBackgroundImageFromInput(String(v ?? '')))"
|
|
87
88
|
/>
|
|
88
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
89
89
|
</label>
|
|
90
90
|
<div class="flex items-center gap-2">
|
|
91
91
|
<label
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ViewportOverflowMode,
|
|
11
11
|
} from "../../utils/viewportPlacement";
|
|
12
12
|
import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
|
|
13
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
13
14
|
|
|
14
15
|
const { t, locale } = useI18n();
|
|
15
16
|
const props = defineProps<{
|
|
@@ -434,7 +435,12 @@ function setNodeCardExpanded(id: string, open: boolean) {
|
|
|
434
435
|
|
|
435
436
|
<template v-if="el.materialType === 'image'">
|
|
436
437
|
<label class="block space-y-1" data-config-field="style.backgroundImageRemoteUrl">
|
|
437
|
-
<div
|
|
438
|
+
<div class="flex items-center gap-1">
|
|
439
|
+
{{ t("panel.config.backgroundImageUrl") }}
|
|
440
|
+
<ConfigHintIcon :label="t('panel.config.backgroundImageUrl')">
|
|
441
|
+
{{ t("panel.config.urlScopeHint") }}
|
|
442
|
+
</ConfigHintIcon>
|
|
443
|
+
</div>
|
|
438
444
|
<Input
|
|
439
445
|
size="small"
|
|
440
446
|
class="font-mono text-[11px]"
|
|
@@ -447,7 +453,6 @@ function setNodeCardExpanded(id: string, open: boolean) {
|
|
|
447
453
|
},
|
|
448
454
|
})"
|
|
449
455
|
/>
|
|
450
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
451
456
|
</label>
|
|
452
457
|
<label class="block space-y-1">
|
|
453
458
|
<div>{{ t("panel.config.objectFit") }}</div>
|
|
@@ -344,6 +344,7 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
|
|
|
344
344
|
@update:open="emit('update:open', $event)"
|
|
345
345
|
>
|
|
346
346
|
<ConfigFieldGroup :title="t('panel.config.groupScene3dPurpose')">
|
|
347
|
+
<template #hint>{{ t("panel.config.scene3dPurposeHint") }}</template>
|
|
347
348
|
<Select
|
|
348
349
|
size="small"
|
|
349
350
|
class="w-full"
|
|
@@ -356,7 +357,6 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
|
|
|
356
357
|
]"
|
|
357
358
|
@update:value="(v) => patch(purposePresetPatch(String(v) as Scene3dPurpose))"
|
|
358
359
|
/>
|
|
359
|
-
<div class="text-[10px] text-gray-500">{{ t("panel.config.scene3dPurposeHint") }}</div>
|
|
360
360
|
</ConfigFieldGroup>
|
|
361
361
|
|
|
362
362
|
<ConfigFieldGroup :title="t('panel.config.groupScene3dModels')">
|
|
@@ -374,7 +374,12 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
|
|
|
374
374
|
/>
|
|
375
375
|
</div>
|
|
376
376
|
<label class="block space-y-1" data-config-field="scene3d.modelUrl">
|
|
377
|
-
<div class="text-[10px] text-gray-500">
|
|
377
|
+
<div class="flex items-center gap-1 text-[10px] text-gray-500">
|
|
378
|
+
{{ t("panel.config.scene3dModelUrl") }}
|
|
379
|
+
<ConfigHintIcon :label="t('panel.config.scene3dModelUrl')">
|
|
380
|
+
{{ t("panel.config.urlScopeHint") }}
|
|
381
|
+
</ConfigHintIcon>
|
|
382
|
+
</div>
|
|
378
383
|
<div class="flex gap-1.5">
|
|
379
384
|
<Input
|
|
380
385
|
size="small"
|
|
@@ -392,7 +397,6 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
|
|
|
392
397
|
{{ t("common.add") }}
|
|
393
398
|
</Button>
|
|
394
399
|
</div>
|
|
395
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
396
400
|
</label>
|
|
397
401
|
<div v-if="config.models.length === 0" class="text-[11px] text-gray-500">
|
|
398
402
|
{{ t("panel.config.scene3dNoModels") }}
|
|
@@ -477,7 +481,7 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
|
|
|
477
481
|
</ConfigFieldGroup>
|
|
478
482
|
|
|
479
483
|
<ConfigFieldGroup :title="t('panel.config.groupScene3dCamera')">
|
|
480
|
-
<
|
|
484
|
+
<template #hint>{{ t("panel.config.scene3dCameraEditDesc") }}</template>
|
|
481
485
|
<div class="grid grid-cols-3 gap-1">
|
|
482
486
|
<Input
|
|
483
487
|
v-for="(axis, index) in ['x', 'y', 'z']"
|
|
@@ -9,6 +9,7 @@ import { getPanelMessages } from "../../constants/messages";
|
|
|
9
9
|
import ConfigColorField from "./ConfigColorField.vue";
|
|
10
10
|
import ConfigFieldGroup from "./ConfigFieldGroup.vue";
|
|
11
11
|
import ConfigSection from "./ConfigSection.vue";
|
|
12
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
12
13
|
|
|
13
14
|
const { t, locale } = useI18n();
|
|
14
15
|
const msgs = () => getPanelMessages(t);
|
|
@@ -89,7 +90,12 @@ function onFileChange(e: Event) {
|
|
|
89
90
|
:default-open="false"
|
|
90
91
|
>
|
|
91
92
|
<label class="block space-y-1" data-config-field="style.backgroundImage">
|
|
92
|
-
<div
|
|
93
|
+
<div class="flex items-center gap-1">
|
|
94
|
+
{{ t("panel.config.backgroundImage") }}
|
|
95
|
+
<ConfigHintIcon :label="t('panel.config.backgroundImage')">
|
|
96
|
+
{{ t("panel.config.urlScopeHint") }}
|
|
97
|
+
</ConfigHintIcon>
|
|
98
|
+
</div>
|
|
93
99
|
<Input
|
|
94
100
|
size="small"
|
|
95
101
|
class="font-mono text-[11px]"
|
|
@@ -97,9 +103,7 @@ function onFileChange(e: Event) {
|
|
|
97
103
|
:disabled="!isEditable"
|
|
98
104
|
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
99
105
|
@update:value="(v: string) => patchStyle(patchBackgroundImageFromInput(String(v ?? '')))"
|
|
100
|
-
/>
|
|
101
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
102
|
-
</label>
|
|
106
|
+
/> </label>
|
|
103
107
|
<div class="flex items-center gap-2">
|
|
104
108
|
<label
|
|
105
109
|
class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
|
|
@@ -7,6 +7,7 @@ import { cssTextLineHeight, cssTextAlignStyle } from "../../utils/panelElementDe
|
|
|
7
7
|
import ConfigColorField from "./ConfigColorField.vue";
|
|
8
8
|
import ConfigFieldGroup from "./ConfigFieldGroup.vue";
|
|
9
9
|
import ConfigSection from "./ConfigSection.vue";
|
|
10
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
10
11
|
|
|
11
12
|
const { t, locale } = useI18n();
|
|
12
13
|
const props = defineProps<{
|
|
@@ -106,7 +107,12 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
|
|
|
106
107
|
}"
|
|
107
108
|
/>
|
|
108
109
|
<label class="block space-y-1">
|
|
109
|
-
<div
|
|
110
|
+
<div class="flex items-center gap-1">
|
|
111
|
+
{{ t("panel.config.textHtmlSource") }}
|
|
112
|
+
<ConfigHintIcon :label="t('panel.config.textHtmlSource')">
|
|
113
|
+
{{ t("panel.config.scopeTemplateHint") }}
|
|
114
|
+
</ConfigHintIcon>
|
|
115
|
+
</div>
|
|
110
116
|
<textarea
|
|
111
117
|
data-config-field="textHtml"
|
|
112
118
|
class="w-full rounded border border-gray-200 bg-white px-2 py-1.5 font-mono text-[11px] leading-relaxed outline-none"
|
|
@@ -116,9 +122,7 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
|
|
|
116
122
|
:disabled="!isEditable"
|
|
117
123
|
:placeholder="t('panel.config.textHtmlSourcePlaceholder')"
|
|
118
124
|
@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>
|
|
125
|
+
/> </label>
|
|
122
126
|
</ConfigFieldGroup>
|
|
123
127
|
<ConfigFieldGroup :title="t('panel.config.groupTextStyle')">
|
|
124
128
|
<label class="block space-y-1">
|
|
@@ -7,6 +7,7 @@ import { readFileAsDataUrl, uploadFileToRemote } from "./shared";
|
|
|
7
7
|
import { getPanelMessages } from "../../constants/messages";
|
|
8
8
|
import ConfigFieldGroup from "./ConfigFieldGroup.vue";
|
|
9
9
|
import ConfigSection from "./ConfigSection.vue";
|
|
10
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
10
11
|
|
|
11
12
|
const { t, locale } = useI18n();
|
|
12
13
|
const msgs = () => getPanelMessages(t);
|
|
@@ -67,7 +68,12 @@ function onVideoFileChange(e: Event) {
|
|
|
67
68
|
>
|
|
68
69
|
<ConfigFieldGroup :title="t('panel.config.groupVideoSource')">
|
|
69
70
|
<label class="block space-y-1" data-config-field="videoRemoteUrl">
|
|
70
|
-
<div
|
|
71
|
+
<div class="flex items-center gap-1">
|
|
72
|
+
{{ t("panel.config.videoUrl") }}
|
|
73
|
+
<ConfigHintIcon :label="t('panel.config.videoUrl')">
|
|
74
|
+
{{ t("panel.config.urlScopeHint") }}
|
|
75
|
+
</ConfigHintIcon>
|
|
76
|
+
</div>
|
|
71
77
|
<Input
|
|
72
78
|
size="small"
|
|
73
79
|
class="font-mono text-[11px]"
|
|
@@ -78,9 +84,7 @@ function onVideoFileChange(e: Event) {
|
|
|
78
84
|
videoRemoteUrl: v || undefined,
|
|
79
85
|
videoSrc: v || element.videoSrc,
|
|
80
86
|
})"
|
|
81
|
-
/>
|
|
82
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
83
|
-
</label>
|
|
87
|
+
/> </label>
|
|
84
88
|
<div class="flex items-center gap-2">
|
|
85
89
|
<label
|
|
86
90
|
class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
|
|
@@ -69,17 +69,13 @@ function closeDropdown() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function refreshForInput(input: HTMLInputElement | HTMLTextAreaElement) {
|
|
72
|
-
if (props.scope === undefined) {
|
|
73
|
-
closeDropdown();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
72
|
const cursor = input.selectionStart ?? input.value.length;
|
|
77
|
-
const parsed = parseScopeAutocomplete(input.value, cursor, props.scope);
|
|
73
|
+
const parsed = parseScopeAutocomplete(input.value, cursor, props.scope ?? {});
|
|
78
74
|
if (!parsed) {
|
|
79
75
|
closeDropdown();
|
|
80
76
|
return;
|
|
81
77
|
}
|
|
82
|
-
const suggestions = getScopeAutocompleteSuggestions(props.scope, parsed);
|
|
78
|
+
const suggestions = getScopeAutocompleteSuggestions(props.scope ?? {}, parsed);
|
|
83
79
|
if (suggestions.length === 0) {
|
|
84
80
|
closeDropdown();
|
|
85
81
|
return;
|
|
@@ -173,7 +169,7 @@ function onPointerDown(event: MouseEvent) {
|
|
|
173
169
|
watch(
|
|
174
170
|
() => toValue(props.containerRef),
|
|
175
171
|
(container, _prev, onCleanup) => {
|
|
176
|
-
if (!container
|
|
172
|
+
if (!container) return;
|
|
177
173
|
|
|
178
174
|
container.addEventListener("focusin", onFocusIn);
|
|
179
175
|
container.addEventListener("input", onInput, true);
|
|
@@ -197,13 +193,14 @@ watch(
|
|
|
197
193
|
watch(
|
|
198
194
|
() => props.scope,
|
|
199
195
|
() => {
|
|
200
|
-
|
|
196
|
+
const current = dropdown.value;
|
|
197
|
+
if (current) refreshForInput(current.input);
|
|
201
198
|
}
|
|
202
199
|
);
|
|
203
200
|
</script>
|
|
204
201
|
|
|
205
202
|
<template>
|
|
206
|
-
<Teleport v-if="dropdown
|
|
203
|
+
<Teleport v-if="dropdown" to="body">
|
|
207
204
|
<div
|
|
208
205
|
data-scope-ac-list
|
|
209
206
|
class="fixed z-[10120] max-h-44 min-w-[140px] overflow-auto rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-md"
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
14
14
|
import { Button, Input, Select } from "ant-design-vue";
|
|
15
15
|
import { computed } from "vue";
|
|
16
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
16
17
|
|
|
17
18
|
const props = withDefaults(
|
|
18
19
|
defineProps<{
|
|
@@ -92,7 +93,12 @@ function patchLeaf(index: number, patch: Partial<ConditionLeaf>) {
|
|
|
92
93
|
<div class="space-y-2">
|
|
93
94
|
<div class="flex items-center justify-between gap-2">
|
|
94
95
|
<label class="min-w-0 flex-1 space-y-1">
|
|
95
|
-
<div class="text-[11px] text-gray-500">
|
|
96
|
+
<div class="flex items-center gap-1 text-[11px] text-gray-500">
|
|
97
|
+
{{ t("panel.config.tableConditionLogic") }}
|
|
98
|
+
<ConfigHintIcon :label="t('panel.config.tableConditionLogic')">
|
|
99
|
+
{{ t("panel.config.tableConditionLogicHint") }}
|
|
100
|
+
</ConfigHintIcon>
|
|
101
|
+
</div>
|
|
96
102
|
<Select
|
|
97
103
|
size="small"
|
|
98
104
|
class="w-full"
|
|
@@ -114,7 +120,6 @@ function patchLeaf(index: number, patch: Partial<ConditionLeaf>) {
|
|
|
114
120
|
{{ t("panel.config.tableConditionAddItem") }}
|
|
115
121
|
</Button>
|
|
116
122
|
</div>
|
|
117
|
-
<div class="text-[10px] text-gray-500">{{ t("panel.config.tableConditionLogicHint") }}</div>
|
|
118
123
|
<div
|
|
119
124
|
v-for="(item, index) in items"
|
|
120
125
|
:key="index"
|
|
@@ -504,10 +504,16 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
|
|
|
504
504
|
</label>
|
|
505
505
|
<details class="rounded-md border border-gray-200 bg-gray-50/60 px-2 py-1.5">
|
|
506
506
|
<summary class="cursor-pointer text-[11px] font-medium text-gray-600">
|
|
507
|
-
|
|
507
|
+
<span class="inline-flex items-center gap-1 align-middle">
|
|
508
|
+
{{ t("panel.config.tableAdvancedData") }}
|
|
509
|
+
<span class="inline-flex" @click.prevent>
|
|
510
|
+
<ConfigHintIcon :label="t('panel.config.tableAdvancedData')">
|
|
511
|
+
{{ t("panel.config.tableAdvancedDataHint") }}
|
|
512
|
+
</ConfigHintIcon>
|
|
513
|
+
</span>
|
|
514
|
+
</span>
|
|
508
515
|
</summary>
|
|
509
516
|
<div class="mt-2 space-y-2 border-t border-gray-100 pt-2">
|
|
510
|
-
<p class="text-[10px] text-gray-500">{{ t("panel.config.tableAdvancedDataHint") }}</p>
|
|
511
517
|
<div class="grid grid-cols-2 gap-2">
|
|
512
518
|
<label class="block space-y-1.5">
|
|
513
519
|
<div class="flex items-center gap-1">
|
|
@@ -1338,7 +1344,7 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
|
|
|
1338
1344
|
<Input
|
|
1339
1345
|
size="small"
|
|
1340
1346
|
:value="String(selectedColumn.column.widgetProps?.progressStatic ?? '')"
|
|
1341
|
-
placeholder="
|
|
1347
|
+
:placeholder="t('panel.config.tableProgressStaticPlaceholder')"
|
|
1342
1348
|
:disabled="!isEditable"
|
|
1343
1349
|
@update:value="
|
|
1344
1350
|
(v: unknown) =>
|
|
@@ -1756,7 +1762,7 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
|
|
|
1756
1762
|
size="small"
|
|
1757
1763
|
:value="selectedColumn.column.displayTemplate ?? ''"
|
|
1758
1764
|
:disabled="!isEditable"
|
|
1759
|
-
placeholder="
|
|
1765
|
+
:placeholder="t('panel.config.tableDisplayTemplatePlaceholder')"
|
|
1760
1766
|
@update:value="
|
|
1761
1767
|
(v: unknown) => {
|
|
1762
1768
|
const s = asSelectString(v).trim();
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
getDefaultSizeByMaterial,
|
|
23
23
|
getDefaultTableConfig,
|
|
24
24
|
getDefaultTextContent,
|
|
25
|
+
localizeLayers,
|
|
26
|
+
localizePanelElement,
|
|
25
27
|
normalizePrimaryLayer,
|
|
26
28
|
randomId,
|
|
27
29
|
} from "../utils/panelElementDefaults";
|
|
@@ -122,7 +124,8 @@ export function usePanelElements() {
|
|
|
122
124
|
const layers = computed(() => {
|
|
123
125
|
const rawLayers =
|
|
124
126
|
(stateRef.value.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
|
|
125
|
-
|
|
127
|
+
void locale.value;
|
|
128
|
+
return localizeLayers(normalizePrimaryLayer(rawLayers), t);
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
const activeLayerId = computed(
|
|
@@ -133,13 +136,26 @@ export function usePanelElements() {
|
|
|
133
136
|
const allElementsCache = shallowRef<PanelElement[]>([]);
|
|
134
137
|
const elementsCache = shallowRef<PanelElement[]>([]);
|
|
135
138
|
|
|
139
|
+
let localizedCache = { locale: locale.value, map: new WeakMap<PanelElement, PanelElement>() };
|
|
140
|
+
|
|
136
141
|
const allElements = computed(() => {
|
|
137
142
|
const next = materializePanelElements(
|
|
138
143
|
stateRef.value.root.children,
|
|
139
144
|
allElementsCache.value
|
|
140
145
|
);
|
|
141
146
|
allElementsCache.value = next;
|
|
142
|
-
|
|
147
|
+
if (localizedCache.locale !== locale.value) {
|
|
148
|
+
localizedCache = { locale: locale.value, map: new WeakMap() };
|
|
149
|
+
}
|
|
150
|
+
const { map } = localizedCache;
|
|
151
|
+
return next.map((el) => {
|
|
152
|
+
let localized = map.get(el);
|
|
153
|
+
if (!localized) {
|
|
154
|
+
localized = localizePanelElement(el, t);
|
|
155
|
+
map.set(el, localized);
|
|
156
|
+
}
|
|
157
|
+
return localized;
|
|
158
|
+
});
|
|
143
159
|
});
|
|
144
160
|
|
|
145
161
|
const elements = computed(() => {
|
|
@@ -5,6 +5,10 @@ export const AbuilderEvents = {
|
|
|
5
5
|
workspaceAdd: "workspace:add",
|
|
6
6
|
/** 同步工作区 */
|
|
7
7
|
workspaceSync: "workspace:sync",
|
|
8
|
+
/** 开始上传接口集合 */
|
|
9
|
+
apiCollectionUploadStart: "api-collection:upload-start",
|
|
10
|
+
/** 上传接口集合成功 */
|
|
11
|
+
apiCollectionUploadSuccess: "api-collection:upload-success",
|
|
8
12
|
} as const;
|
|
9
13
|
|
|
10
14
|
export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
|
|
@@ -15,9 +19,25 @@ export type WorkspaceData = WorkspaceProjectRecord;
|
|
|
15
19
|
export type WorkspaceAddEventPayload = WorkspaceData;
|
|
16
20
|
export type WorkspaceSyncEventPayload = WorkspaceData;
|
|
17
21
|
|
|
22
|
+
export type ApiCollectionUploadStartEventPayload = {
|
|
23
|
+
fileNames: string[];
|
|
24
|
+
count: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ApiCollectionUploadSuccessEventPayload = {
|
|
28
|
+
collections: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
apiCount: number;
|
|
32
|
+
persistLocal: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
18
36
|
export type AbuilderEventPayloadMap = {
|
|
19
37
|
[AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
|
|
20
38
|
[AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
|
|
39
|
+
[AbuilderEvents.apiCollectionUploadStart]: ApiCollectionUploadStartEventPayload;
|
|
40
|
+
[AbuilderEvents.apiCollectionUploadSuccess]: ApiCollectionUploadSuccessEventPayload;
|
|
21
41
|
};
|
|
22
42
|
|
|
23
43
|
type EventCallback = (payload: unknown) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TranslateFn } from "@arronqzy/i18n";
|
|
2
|
-
import { tForLocale } from "@arronqzy/i18n";
|
|
2
|
+
import { relocalizeMessage, tForLocale } from "@arronqzy/i18n";
|
|
3
3
|
import { createDefaultTableConfig, type PanelTableConfig } from "@arronqzy/view-table";
|
|
4
4
|
import { createDefaultScene3dConfig } from "@arronqzy/view-scene3d";
|
|
5
5
|
import type { PanelChartConfig, PanelLayer, PanelElement } from "../types";
|
|
@@ -35,6 +35,52 @@ export function getDefaultNodeName(
|
|
|
35
35
|
return key ? t(key) : materialType;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const DEFAULT_LAYER_NAME_KEYS = [
|
|
39
|
+
"panel.defaults.layer1",
|
|
40
|
+
"panel.defaults.layerN",
|
|
41
|
+
"panel.defaults.mappingLayerN",
|
|
42
|
+
"panel.defaults.layerRandom",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Persisted default layer names ("图层1" / "Layer 2") follow the current locale. */
|
|
46
|
+
export function localizeLayerName(name: string, t: TranslateFn): string {
|
|
47
|
+
return relocalizeMessage(name, DEFAULT_LAYER_NAME_KEYS, t);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function localizeLayers(layers: PanelLayer[], t: TranslateFn): PanelLayer[] {
|
|
51
|
+
return layers.map((layer) => {
|
|
52
|
+
const name = localizeLayerName(layer.name, t);
|
|
53
|
+
return name === layer.name ? layer : { ...layer, name };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_TEXT_HTML_RE = /^<p>(.*)<\/p>$/s;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default node name, chart title and text content are stored in the creation locale;
|
|
61
|
+
* re-render them in the current one. Returns the same object when nothing changes.
|
|
62
|
+
*/
|
|
63
|
+
export function localizePanelElement(element: PanelElement, t: TranslateFn): PanelElement {
|
|
64
|
+
const key = DEFAULT_NODE_NAME_KEYS[element.materialType ?? ""];
|
|
65
|
+
if (!key) return element;
|
|
66
|
+
const name = element.name ? relocalizeMessage(element.name, key, t) : element.name;
|
|
67
|
+
const title = element.chart?.title ? relocalizeMessage(element.chart.title, key, t) : undefined;
|
|
68
|
+
let textHtml = element.textHtml;
|
|
69
|
+
const textInner = textHtml ? DEFAULT_TEXT_HTML_RE.exec(textHtml)?.[1] : undefined;
|
|
70
|
+
if (textInner) {
|
|
71
|
+
const localized = relocalizeMessage(textInner, "panel.defaults.doubleClickTextHtml", t);
|
|
72
|
+
if (localized !== textInner) textHtml = `<p>${localized}</p>`;
|
|
73
|
+
}
|
|
74
|
+
const titleChanged = title !== undefined && title !== element.chart?.title;
|
|
75
|
+
if (name === element.name && !titleChanged && textHtml === element.textHtml) return element;
|
|
76
|
+
return {
|
|
77
|
+
...element,
|
|
78
|
+
name,
|
|
79
|
+
textHtml,
|
|
80
|
+
...(titleChanged && element.chart ? { chart: { ...element.chart, title } } : null),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
38
84
|
export function getDefaultLayer(t: TranslateFn = tForLocale("zh-CN")): PanelLayer {
|
|
39
85
|
return {
|
|
40
86
|
id: DEFAULT_LAYER_ID,
|
|
@@ -1,6 +1,10 @@
|
|
|
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 {
|
|
5
|
+
createSystemRoot,
|
|
6
|
+
isDayjsValue,
|
|
7
|
+
} from "@arronqzy/blueprint-dsl";
|
|
4
8
|
import { materializeChartLabelsFromScope, materializeChartValuesFromScope } from "./scope-template-chart";
|
|
5
9
|
import {
|
|
6
10
|
SCOPE_SPREAD_TEMPLATE_RE,
|
|
@@ -11,12 +15,12 @@ import {
|
|
|
11
15
|
resolveSpreadScopePath,
|
|
12
16
|
} from "./scope-template-spread";
|
|
13
17
|
|
|
14
|
-
const
|
|
18
|
+
const TEMPLATE_TOKEN_RE = /\{((?:scope|system)[^}]*)\}/g;
|
|
15
19
|
|
|
16
20
|
export function hasScopeTemplate(value: string): boolean {
|
|
17
|
-
|
|
21
|
+
TEMPLATE_TOKEN_RE.lastIndex = 0;
|
|
18
22
|
SCOPE_SPREAD_TEMPLATE_RE.lastIndex = 0;
|
|
19
|
-
return
|
|
23
|
+
return TEMPLATE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export function evaluateScopeExpression(
|
|
@@ -26,8 +30,13 @@ export function evaluateScopeExpression(
|
|
|
26
30
|
const trimmed = expression.trim();
|
|
27
31
|
if (!trimmed) return undefined;
|
|
28
32
|
try {
|
|
29
|
-
const
|
|
30
|
-
|
|
33
|
+
const system = createSystemRoot();
|
|
34
|
+
const fn = new Function(
|
|
35
|
+
"scope",
|
|
36
|
+
"system",
|
|
37
|
+
`"use strict"; return (${trimmed});`
|
|
38
|
+
);
|
|
39
|
+
return fn(scope, system);
|
|
31
40
|
} catch {
|
|
32
41
|
return undefined;
|
|
33
42
|
}
|
|
@@ -39,6 +48,9 @@ function stringifyScopeValue(value: unknown): string {
|
|
|
39
48
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
40
49
|
return String(value);
|
|
41
50
|
}
|
|
51
|
+
if (isDayjsValue(value)) {
|
|
52
|
+
return value.toISOString();
|
|
53
|
+
}
|
|
42
54
|
try {
|
|
43
55
|
return JSON.stringify(value);
|
|
44
56
|
} catch {
|
|
@@ -60,8 +72,8 @@ export function evaluateScopeTemplate(
|
|
|
60
72
|
}
|
|
61
73
|
);
|
|
62
74
|
|
|
63
|
-
// Only `{scope...}`
|
|
64
|
-
return withSpread.replace(
|
|
75
|
+
// Only `{scope...}` / `{system...}` — keep ECharts / CSS braces like `{b}` intact.
|
|
76
|
+
return withSpread.replace(TEMPLATE_TOKEN_RE, (_: string, rawExpr: string) =>
|
|
65
77
|
stringifyScopeValue(evaluateScopeExpression(rawExpr, scope))
|
|
66
78
|
);
|
|
67
79
|
}
|
|
@@ -91,7 +103,7 @@ function materializeTemplateValue(template: string, scope: unknown): unknown {
|
|
|
91
103
|
);
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
const singleMatch = /^\{(scope[^}]*)\}$/.exec(trimmed);
|
|
106
|
+
const singleMatch = /^\{((?:scope|system)[^}]*)\}$/.exec(trimmed);
|
|
95
107
|
if (singleMatch) {
|
|
96
108
|
return evaluateScopeExpression(singleMatch[1]!, scope);
|
|
97
109
|
}
|