@arronqzy/vue-view 0.1.9 → 0.1.11
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 +15 -1
- package/package.json +8 -8
- package/src/index.ts +26 -0
- package/src/panel/VueViewOnlinePreview.vue +68 -26
- package/src/panel/VueViewPanel.vue +15 -10
- package/src/panel/components/ElementsLayer.vue +4 -2
- package/src/panel/components/MaterialPreview.vue +8 -0
- package/src/panel/components/MaterialSidebar.vue +15 -18
- package/src/panel/components/MaterialSidebarTreeNode.vue +4 -2
- package/src/panel/components/PanelConfigSidebar.vue +17 -1
- package/src/panel/components/WorkspaceConfigSidebar.vue +1 -0
- package/src/panel/components/config/PanelConfigMultiSelect.vue +30 -1
- package/src/panel/components/config/PanelConfigViewportSection.vue +114 -0
- package/src/panel/components/elementsLayerNodes.ts +75 -17
- package/src/panel/components/materialSidebarData.ts +2 -0
- package/src/panel/hooks/usePanelElements.ts +10 -4
- package/src/panel/hooks/useWorkspaceProjects.ts +144 -34
- package/src/panel/library/event-subscription.ts +60 -0
- package/src/panel/library/parse-workspace-data.ts +142 -0
- package/src/panel/library/workspace-project-cache.ts +13 -4
- package/src/panel/library/workspace-project-db.ts +24 -13
- package/src/panel/library/workspace-project-sync.ts +17 -6
- package/src/panel/parseOnlinePreviewSearchParams.ts +3 -0
- package/src/panel/types.ts +4 -0
- package/src/panel/utils/outputScale.ts +15 -2
- package/src/panel/utils/panelElementDefaults.ts +10 -0
- package/src/panel/utils/panelElementNodes.ts +5 -3
- package/src/panel/utils/panelStateIO.ts +18 -0
- package/src/panel/utils/viewportPlacement.ts +73 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
|
+
import { Select } from "ant-design-vue";
|
|
4
|
+
import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
|
|
5
|
+
import {
|
|
6
|
+
VIEWPORT_OVERFLOW_MODES,
|
|
7
|
+
normalizeViewportOverflow,
|
|
8
|
+
type ViewportOverflowMode,
|
|
9
|
+
} from "../../utils/viewportPlacement";
|
|
10
|
+
import ConfigFieldGroup from "./ConfigFieldGroup.vue";
|
|
11
|
+
import ConfigSection from "./ConfigSection.vue";
|
|
12
|
+
|
|
13
|
+
const { t } = useI18n();
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
element: PanelElement;
|
|
16
|
+
layers: PanelLayer[];
|
|
17
|
+
isEditable: boolean;
|
|
18
|
+
open: boolean;
|
|
19
|
+
forceOpen?: boolean;
|
|
20
|
+
setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
|
|
21
|
+
updateElement: (
|
|
22
|
+
id: string,
|
|
23
|
+
patch: Partial<PanelElement>,
|
|
24
|
+
options?: { batchId?: string; meta?: Record<string, unknown> }
|
|
25
|
+
) => void;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
"update:open": [value: boolean];
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
const overflowLabelKey: Record<ViewportOverflowMode, string> = {
|
|
33
|
+
"scroll-x": "panel.config.viewportOverflowScrollX",
|
|
34
|
+
"scroll-y": "panel.config.viewportOverflowScrollY",
|
|
35
|
+
scroll: "panel.config.viewportOverflowScroll",
|
|
36
|
+
clip: "panel.config.viewportOverflowClip",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function patch(next: Partial<PanelElement>) {
|
|
40
|
+
props.updateElement(props.element.id, next);
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<ConfigSection
|
|
46
|
+
:title="t('panel.config.sectionViewport')"
|
|
47
|
+
:open="open"
|
|
48
|
+
:force-open="forceOpen"
|
|
49
|
+
@update:open="emit('update:open', $event)"
|
|
50
|
+
>
|
|
51
|
+
<template #hint>
|
|
52
|
+
{{ t("panel.config.viewportHint") }}
|
|
53
|
+
</template>
|
|
54
|
+
<ConfigFieldGroup :title="t('panel.config.groupRefSource')">
|
|
55
|
+
<label class="block space-y-1">
|
|
56
|
+
<div>{{ t("panel.config.refLayer") }}</div>
|
|
57
|
+
<Select
|
|
58
|
+
size="small"
|
|
59
|
+
class="w-full"
|
|
60
|
+
:value="element.refLayerId ?? '__none__'"
|
|
61
|
+
:disabled="!isEditable"
|
|
62
|
+
@update:value="(v) => patch({ refLayerId: v === '__none__' ? undefined : String(v) })"
|
|
63
|
+
>
|
|
64
|
+
<Select.Option value="__none__">{{ t("panel.config.noneNoRef") }}</Select.Option>
|
|
65
|
+
<Select.Option
|
|
66
|
+
v-for="layer in layers.filter((l) => l.id !== element.layerId)"
|
|
67
|
+
:key="layer.id"
|
|
68
|
+
:value="layer.id"
|
|
69
|
+
>
|
|
70
|
+
{{ layer.name }}
|
|
71
|
+
</Select.Option>
|
|
72
|
+
</Select>
|
|
73
|
+
</label>
|
|
74
|
+
</ConfigFieldGroup>
|
|
75
|
+
<ConfigFieldGroup
|
|
76
|
+
:title="t('panel.config.groupCopyStrategy')"
|
|
77
|
+
collapsible
|
|
78
|
+
:default-open="false"
|
|
79
|
+
>
|
|
80
|
+
<template #hint>
|
|
81
|
+
{{ t("panel.config.copyStrategyHint") }}
|
|
82
|
+
</template>
|
|
83
|
+
<label class="block space-y-1">
|
|
84
|
+
<div>{{ t("panel.config.copyMode") }}</div>
|
|
85
|
+
<Select
|
|
86
|
+
size="small"
|
|
87
|
+
class="w-full"
|
|
88
|
+
:value="element.refCopyMode ?? 'shallow'"
|
|
89
|
+
:disabled="!isEditable"
|
|
90
|
+
@update:value="(v) => setReferenceCopyMode?.(element.id, v as ReferenceCopyMode)"
|
|
91
|
+
>
|
|
92
|
+
<Select.Option value="shallow">{{ t("panel.config.shallowFollow") }}</Select.Option>
|
|
93
|
+
<Select.Option value="deep">{{ t("panel.config.deepFreeze") }}</Select.Option>
|
|
94
|
+
</Select>
|
|
95
|
+
</label>
|
|
96
|
+
</ConfigFieldGroup>
|
|
97
|
+
<ConfigFieldGroup :title="t('panel.config.groupViewportOverflow')">
|
|
98
|
+
<label class="block space-y-1">
|
|
99
|
+
<div>{{ t("panel.config.viewportOverflow") }}</div>
|
|
100
|
+
<Select
|
|
101
|
+
size="small"
|
|
102
|
+
class="w-full"
|
|
103
|
+
:value="normalizeViewportOverflow(element.viewportOverflow)"
|
|
104
|
+
:disabled="!isEditable"
|
|
105
|
+
@update:value="(v) => patch({ viewportOverflow: v as ViewportOverflowMode })"
|
|
106
|
+
>
|
|
107
|
+
<Select.Option v-for="mode in VIEWPORT_OVERFLOW_MODES" :key="mode" :value="mode">
|
|
108
|
+
{{ t(overflowLabelKey[mode]) }}
|
|
109
|
+
</Select.Option>
|
|
110
|
+
</Select>
|
|
111
|
+
</label>
|
|
112
|
+
</ConfigFieldGroup>
|
|
113
|
+
</ConfigSection>
|
|
114
|
+
</template>
|
|
@@ -16,6 +16,12 @@ 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 {
|
|
20
|
+
isViewportNode,
|
|
21
|
+
normalizeViewportOverflow,
|
|
22
|
+
viewportOverflowStyle,
|
|
23
|
+
viewportWindowContentScale,
|
|
24
|
+
} from "../utils/viewportPlacement";
|
|
19
25
|
import TableNodeContent from "./table/TableNodeContent.vue";
|
|
20
26
|
import Scene3dNodeContent from "@arronqzy/view-scene3d/vue";
|
|
21
27
|
|
|
@@ -712,15 +718,25 @@ export const ReferenceNodeContent = defineComponent({
|
|
|
712
718
|
const sourceHeight = Math.max(1, maxY - minY);
|
|
713
719
|
const innerW = Math.max(1, p.element.width);
|
|
714
720
|
const innerH = Math.max(1, p.element.height);
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
|
|
721
|
+
const windowFit = isViewportNode(p.element);
|
|
722
|
+
const windowScale = windowFit
|
|
723
|
+
? viewportWindowContentScale(p.element, p.allElements)
|
|
724
|
+
: { scaleX: 1, scaleY: 1 };
|
|
725
|
+
const scaleX = windowFit
|
|
726
|
+
? windowScale.scaleX
|
|
727
|
+
: Math.max(0.05, Math.min(innerW / sourceWidth, innerH / sourceHeight));
|
|
728
|
+
const scaleY = windowFit ? windowScale.scaleY : scaleX;
|
|
729
|
+
const mappedW = sourceWidth * scaleX;
|
|
730
|
+
const mappedH = sourceHeight * scaleY;
|
|
718
731
|
return {
|
|
719
732
|
minX,
|
|
720
733
|
minY,
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
734
|
+
scaleX,
|
|
735
|
+
scaleY,
|
|
736
|
+
sourceWidth: mappedW,
|
|
737
|
+
sourceHeight: mappedH,
|
|
738
|
+
offsetX: windowFit ? 0 : (innerW - mappedW) / 2,
|
|
739
|
+
offsetY: windowFit ? 0 : (innerH - mappedH) / 2,
|
|
724
740
|
};
|
|
725
741
|
});
|
|
726
742
|
|
|
@@ -739,20 +755,27 @@ export const ReferenceNodeContent = defineComponent({
|
|
|
739
755
|
]);
|
|
740
756
|
}
|
|
741
757
|
|
|
742
|
-
|
|
743
|
-
|
|
758
|
+
const overflowCss = isViewportNode(p.element)
|
|
759
|
+
? viewportOverflowStyle(normalizeViewportOverflow(p.element.viewportOverflow))
|
|
760
|
+
: { overflowX: "hidden" as const, overflowY: "hidden" as const };
|
|
761
|
+
const canScroll =
|
|
762
|
+
isViewportNode(p.element) &&
|
|
763
|
+
(overflowCss.overflowX === "auto" || overflowCss.overflowY === "auto");
|
|
764
|
+
|
|
765
|
+
const innerNodes = sourceNodes.value.map((node) => {
|
|
744
766
|
const box = getNodeAABB(node);
|
|
745
|
-
const left = layout.value!.offsetX + (box.left - layout.value!.minX) * layout.value!.
|
|
746
|
-
const top = layout.value!.offsetY + (box.top - layout.value!.minY) * layout.value!.
|
|
747
|
-
const width = Math.max(12, node.width * layout.value!.
|
|
748
|
-
const height = Math.max(10, node.height * layout.value!.
|
|
749
|
-
const boxWidth = Math.max(12, (box.right - box.left) * layout.value!.
|
|
750
|
-
const boxHeight = Math.max(10, (box.bottom - box.top) * layout.value!.
|
|
767
|
+
const left = layout.value!.offsetX + (box.left - layout.value!.minX) * layout.value!.scaleX;
|
|
768
|
+
const top = layout.value!.offsetY + (box.top - layout.value!.minY) * layout.value!.scaleY;
|
|
769
|
+
const width = Math.max(12, node.width * layout.value!.scaleX);
|
|
770
|
+
const height = Math.max(10, node.height * layout.value!.scaleY);
|
|
771
|
+
const boxWidth = Math.max(12, (box.right - box.left) * layout.value!.scaleX);
|
|
772
|
+
const boxHeight = Math.max(10, (box.bottom - box.top) * layout.value!.scaleY);
|
|
751
773
|
return h(
|
|
752
774
|
"div",
|
|
753
775
|
{
|
|
754
776
|
key: node.id,
|
|
755
777
|
class: "absolute overflow-visible",
|
|
778
|
+
"data-viewport-item": isViewportNode(p.element) ? "true" : undefined,
|
|
756
779
|
style: { left, top, width: boxWidth, height: boxHeight },
|
|
757
780
|
},
|
|
758
781
|
[
|
|
@@ -760,6 +783,7 @@ export const ReferenceNodeContent = defineComponent({
|
|
|
760
783
|
"div",
|
|
761
784
|
{
|
|
762
785
|
class: "absolute",
|
|
786
|
+
"data-viewport-item": isViewportNode(p.element) ? "true" : undefined,
|
|
763
787
|
style: {
|
|
764
788
|
left: (boxWidth - width) / 2,
|
|
765
789
|
top: (boxHeight - height) / 2,
|
|
@@ -777,7 +801,7 @@ export const ReferenceNodeContent = defineComponent({
|
|
|
777
801
|
previewLayoutKey: p.previewLayoutKey,
|
|
778
802
|
previewMode: p.previewMode,
|
|
779
803
|
})
|
|
780
|
-
: node.materialType === "reference"
|
|
804
|
+
: node.materialType === "reference" || node.materialType === "viewport"
|
|
781
805
|
? h(ReferenceNodeContent as Component, {
|
|
782
806
|
element: node,
|
|
783
807
|
allElements: p.allElements,
|
|
@@ -800,8 +824,42 @@ export const ReferenceNodeContent = defineComponent({
|
|
|
800
824
|
),
|
|
801
825
|
]
|
|
802
826
|
);
|
|
803
|
-
})
|
|
804
|
-
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
return h(
|
|
830
|
+
"div",
|
|
831
|
+
{
|
|
832
|
+
class: "relative h-full w-full",
|
|
833
|
+
style: {
|
|
834
|
+
overflowX: overflowCss.overflowX,
|
|
835
|
+
overflowY: overflowCss.overflowY,
|
|
836
|
+
pointerEvents: isViewportNode(p.element) ? "auto" : "none",
|
|
837
|
+
},
|
|
838
|
+
onWheel: canScroll
|
|
839
|
+
? (e: WheelEvent) => {
|
|
840
|
+
e.stopPropagation();
|
|
841
|
+
}
|
|
842
|
+
: undefined,
|
|
843
|
+
},
|
|
844
|
+
[
|
|
845
|
+
h(
|
|
846
|
+
"div",
|
|
847
|
+
{
|
|
848
|
+
class: "pointer-events-none relative",
|
|
849
|
+
"data-viewport-sizer": isViewportNode(p.element) ? "true" : undefined,
|
|
850
|
+
style: isViewportNode(p.element)
|
|
851
|
+
? {
|
|
852
|
+
width: layout.value.sourceWidth,
|
|
853
|
+
height: layout.value.sourceHeight,
|
|
854
|
+
minWidth: "100%",
|
|
855
|
+
minHeight: "100%",
|
|
856
|
+
}
|
|
857
|
+
: { width: "100%", height: "100%" },
|
|
858
|
+
},
|
|
859
|
+
innerNodes
|
|
860
|
+
),
|
|
861
|
+
]
|
|
862
|
+
);
|
|
805
863
|
};
|
|
806
864
|
},
|
|
807
865
|
}) as Component;
|
|
@@ -26,6 +26,7 @@ const MATERIAL_LABEL_KEYS: Record<string, string> = {
|
|
|
26
26
|
text: "panel.material.text",
|
|
27
27
|
rect: "panel.material.rect",
|
|
28
28
|
grid: "panel.material.grid",
|
|
29
|
+
viewport: "panel.material.viewport",
|
|
29
30
|
image: "panel.material.image",
|
|
30
31
|
video: "panel.material.video",
|
|
31
32
|
audio: "panel.material.audio",
|
|
@@ -68,6 +69,7 @@ export function getDefaultCategories(t: TranslateFn): MaterialCategory[] {
|
|
|
68
69
|
{ id: "geometry", title: t("panel.material.geometry") },
|
|
69
70
|
{ id: "scene3d", title: t("panel.material.scene3d") },
|
|
70
71
|
{ id: "grid", title: t("panel.material.grid") },
|
|
72
|
+
{ id: "viewport", title: t("panel.material.viewport") },
|
|
71
73
|
{ id: "image", title: t("panel.material.image") },
|
|
72
74
|
{ id: "reference", title: t("panel.material.reference") },
|
|
73
75
|
],
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
DEFAULT_LAYER_ID,
|
|
17
17
|
getDefaultChartConfig,
|
|
18
18
|
getDefaultGridConfig,
|
|
19
|
+
getDefaultViewportConfig,
|
|
19
20
|
getDefaultNodeName,
|
|
20
21
|
getDefaultScene3dConfigForMaterial,
|
|
21
22
|
getDefaultSizeByMaterial,
|
|
@@ -569,7 +570,10 @@ export function usePanelElements() {
|
|
|
569
570
|
.filter((n) => isPanelElementNode(n) && n.props)
|
|
570
571
|
.map((n) => n.props as PanelElement);
|
|
571
572
|
const desiredMode = options?.referenceCopyMode;
|
|
572
|
-
if (
|
|
573
|
+
if (
|
|
574
|
+
(nextProps.materialType === "reference" || nextProps.materialType === "viewport") &&
|
|
575
|
+
desiredMode
|
|
576
|
+
) {
|
|
573
577
|
if (desiredMode === "deep" && nextProps.refLayerId) {
|
|
574
578
|
nextProps.refCopyMode = "deep";
|
|
575
579
|
nextProps.refSnapshot = buildDeepReferenceSnapshot(all, nextProps.refLayerId);
|
|
@@ -611,7 +615,9 @@ export function usePanelElements() {
|
|
|
611
615
|
name: getDefaultNodeName(materialType, t),
|
|
612
616
|
...getDefaultTextContent(materialType, t),
|
|
613
617
|
...getDefaultGridConfig(materialType),
|
|
614
|
-
|
|
618
|
+
...getDefaultViewportConfig(materialType),
|
|
619
|
+
refCopyMode:
|
|
620
|
+
materialType === "reference" || materialType === "viewport" ? "shallow" : undefined,
|
|
615
621
|
chart: getDefaultChartConfig(materialType, t),
|
|
616
622
|
table: getDefaultTableConfig(materialType, t),
|
|
617
623
|
geometryShape: materialType === "geometry" ? "rect" : undefined,
|
|
@@ -712,7 +718,7 @@ export function usePanelElements() {
|
|
|
712
718
|
.filter((n) => isPanelElementNode(n) && n.props)
|
|
713
719
|
.map((n) => n.props as PanelElement);
|
|
714
720
|
const target = all.find((el) => el.id === id);
|
|
715
|
-
if (!target || target.materialType !== "reference") return;
|
|
721
|
+
if (!target || (target.materialType !== "reference" && target.materialType !== "viewport")) return;
|
|
716
722
|
const currentLayers =
|
|
717
723
|
(current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
|
|
718
724
|
const layer = currentLayers.find((l) => l.id === target.layerId);
|
|
@@ -904,7 +910,7 @@ export function usePanelElements() {
|
|
|
904
910
|
if (target.isMapping) return false;
|
|
905
911
|
const willBeDeleted = mode === "remove" && el.layerId === layerId;
|
|
906
912
|
if (willBeDeleted) return false;
|
|
907
|
-
if (el.materialType !== "reference") return false;
|
|
913
|
+
if (el.materialType !== "reference" && el.materialType !== "viewport") return false;
|
|
908
914
|
if (el.refLayerId !== layerId) return false;
|
|
909
915
|
return (el.refCopyMode ?? "shallow") !== "deep";
|
|
910
916
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed, onMounted, onUnmounted, ref, shallowRef, toValue, watch, type MaybeRefOrGetter } from "vue";
|
|
2
2
|
import {
|
|
3
3
|
BlueprintGraph,
|
|
4
|
+
parseBlueprintData,
|
|
4
5
|
type BlueprintDocument,
|
|
5
6
|
type BlueprintMetaDraft,
|
|
6
7
|
} from "@arronqzy/vue-blueprint";
|
|
@@ -23,6 +24,8 @@ import {
|
|
|
23
24
|
type WorkspaceSnapshot,
|
|
24
25
|
} from "../library/workspace-snapshot";
|
|
25
26
|
import { runBusyTask } from "../utils/async-work";
|
|
27
|
+
import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
|
|
28
|
+
import { AbuilderEvents, emitAbuilderEvent } from "../library/event-subscription";
|
|
26
29
|
|
|
27
30
|
export type UseWorkspaceProjectsOptions = {
|
|
28
31
|
exportPanelData: () => State;
|
|
@@ -39,13 +42,21 @@ export type UseWorkspaceProjectsOptions = {
|
|
|
39
42
|
panelRevision: MaybeRefOrGetter<unknown>;
|
|
40
43
|
/** 工作区记录应用到编辑器后的回调(用于恢复蓝图面板等 UI 状态) */
|
|
41
44
|
onProjectApplied?: (record: WorkspaceProjectRecord) => void;
|
|
45
|
+
/** 外部传入的初始工作区,挂载后自动加载(不触发 onProjectApplied) */
|
|
46
|
+
initialWorkspace?: MaybeRefOrGetter<WorkspaceProjectRecord | null | undefined>;
|
|
47
|
+
nameSpace?: MaybeRefOrGetter<string | null | undefined>;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
|
-
export function buildOnlinePreviewUrl(
|
|
50
|
+
export function buildOnlinePreviewUrl(
|
|
51
|
+
projectId: string,
|
|
52
|
+
nameSpace?: string | null
|
|
53
|
+
): string {
|
|
45
54
|
const url = new URL(window.location.href);
|
|
46
55
|
url.search = "";
|
|
47
56
|
url.searchParams.set("preview", "online");
|
|
48
57
|
url.searchParams.set("projectId", projectId);
|
|
58
|
+
const ns = (nameSpace ?? "").trim();
|
|
59
|
+
if (ns) url.searchParams.set("ns", ns);
|
|
49
60
|
const previewInstanceId =
|
|
50
61
|
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
51
62
|
? crypto.randomUUID()
|
|
@@ -54,8 +65,31 @@ export function buildOnlinePreviewUrl(projectId: string): string {
|
|
|
54
65
|
return url.toString();
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
function previewWindowName(projectId: string, nameSpace?: string | null): string {
|
|
69
|
+
const ns = (nameSpace ?? "").trim();
|
|
70
|
+
return ns ? `abuilder-preview-${ns}-${projectId}` : `abuilder-preview-${projectId}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasWorkspacePayload(
|
|
74
|
+
record?: WorkspaceProjectRecord | null
|
|
75
|
+
): record is WorkspaceProjectRecord {
|
|
76
|
+
if (!record || typeof record !== "object") return false;
|
|
77
|
+
return Boolean(
|
|
78
|
+
record.panelState ||
|
|
79
|
+
record.blueprintDocument ||
|
|
80
|
+
record.id ||
|
|
81
|
+
record.productName ||
|
|
82
|
+
record.titleIconDataUrl
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function workspaceApplyKey(record: WorkspaceProjectRecord): string {
|
|
87
|
+
return `${record.id ?? "anon"}:${record.updatedAt ?? 0}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
58
91
|
const { t, locale } = useI18nOptional();
|
|
92
|
+
const nameSpace = () => toValue(options.nameSpace) ?? null;
|
|
59
93
|
const projects = ref<WorkspaceProjectListItem[]>([]);
|
|
60
94
|
const activeProjectId = ref<string | null>(null);
|
|
61
95
|
const activeProjectName = ref<string | null>(null);
|
|
@@ -77,7 +111,8 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
77
111
|
|
|
78
112
|
function resolveProjectName() {
|
|
79
113
|
void locale.value;
|
|
80
|
-
|
|
114
|
+
const name = toValue(options.productName);
|
|
115
|
+
return (typeof name === "string" ? name : "").trim() || t("panel.defaults.unnamedProduct");
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
function buildCurrentSnapshot(): WorkspaceSnapshot {
|
|
@@ -90,13 +125,15 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
90
125
|
}
|
|
91
126
|
|
|
92
127
|
async function refreshProjects() {
|
|
93
|
-
projects.value = await listWorkspaceProjects();
|
|
128
|
+
projects.value = await listWorkspaceProjects(nameSpace());
|
|
94
129
|
}
|
|
95
130
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
131
|
+
watch(
|
|
132
|
+
() => nameSpace(),
|
|
133
|
+
() => {
|
|
134
|
+
void refreshProjects();
|
|
135
|
+
}
|
|
136
|
+
);
|
|
100
137
|
|
|
101
138
|
onUnmounted(() => {
|
|
102
139
|
if (previewPollTimer !== undefined) window.clearInterval(previewPollTimer);
|
|
@@ -122,36 +159,104 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
122
159
|
return !workspaceSnapshotsEqual(buildCurrentSnapshot(), syncedSnapshotRef.value);
|
|
123
160
|
});
|
|
124
161
|
|
|
125
|
-
function applyProjectRecord(
|
|
162
|
+
function applyProjectRecord(
|
|
163
|
+
record: WorkspaceProjectRecord,
|
|
164
|
+
applyOptions?: { notifyApplied?: boolean }
|
|
165
|
+
) {
|
|
166
|
+
const parsedBlueprint = parseBlueprintData(record.blueprintDocument);
|
|
126
167
|
const blueprintDocument =
|
|
127
|
-
|
|
168
|
+
parsedBlueprint.value ?? BlueprintGraph.empty().document;
|
|
128
169
|
const blueprintMeta: BlueprintMetaDraft = {
|
|
129
170
|
name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
|
|
130
171
|
remark: record.blueprintMeta?.remark ?? "",
|
|
131
172
|
};
|
|
173
|
+
const panelState =
|
|
174
|
+
normalizeImportedPanelState(record.panelState) ?? createEmptyPanelState();
|
|
175
|
+
const productName =
|
|
176
|
+
typeof record.productName === "string" && record.productName.trim()
|
|
177
|
+
? record.productName
|
|
178
|
+
: t("panel.defaults.unnamedProduct");
|
|
179
|
+
const titleIconDataUrl = record.titleIconDataUrl ?? "";
|
|
132
180
|
|
|
133
|
-
options.importPanelData(
|
|
181
|
+
options.importPanelData(panelState);
|
|
134
182
|
options.setBlueprintGraph(BlueprintGraph.fromDocument(blueprintDocument));
|
|
135
183
|
options.setBlueprintMeta(blueprintMeta);
|
|
136
|
-
options.setProductName(
|
|
137
|
-
options.setTitleIconDataUrl(
|
|
138
|
-
activeProjectId.value = record.id;
|
|
139
|
-
activeProjectName.value = record.name;
|
|
184
|
+
options.setProductName(productName);
|
|
185
|
+
options.setTitleIconDataUrl(titleIconDataUrl);
|
|
186
|
+
activeProjectId.value = record.id || null;
|
|
187
|
+
activeProjectName.value = record.name ?? null;
|
|
140
188
|
syncedSnapshotRef.value = cloneWorkspaceSnapshot({
|
|
141
189
|
blueprintDocument,
|
|
142
190
|
blueprintMeta,
|
|
143
|
-
productName
|
|
144
|
-
titleIconDataUrl
|
|
191
|
+
productName,
|
|
192
|
+
titleIconDataUrl,
|
|
145
193
|
});
|
|
146
194
|
syncedPanelRevision = toValue(options.panelRevision);
|
|
147
195
|
pendingRevisionSync = true;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
196
|
+
if (applyOptions?.notifyApplied !== false) {
|
|
197
|
+
options.onProjectApplied?.({
|
|
198
|
+
...record,
|
|
199
|
+
blueprintDocument,
|
|
200
|
+
blueprintMeta,
|
|
201
|
+
panelState,
|
|
202
|
+
productName,
|
|
203
|
+
titleIconDataUrl: titleIconDataUrl || undefined,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function applyEmptyWorkspace() {
|
|
209
|
+
options.importPanelData(createEmptyPanelState());
|
|
210
|
+
options.setBlueprintGraph(BlueprintGraph.empty());
|
|
211
|
+
options.setBlueprintMeta({
|
|
212
|
+
name: t("panel.defaults.unnamedBlueprint"),
|
|
213
|
+
remark: "",
|
|
152
214
|
});
|
|
215
|
+
options.setProductName(t("panel.defaults.unnamedProduct"));
|
|
216
|
+
options.setTitleIconDataUrl("");
|
|
217
|
+
activeProjectId.value = null;
|
|
218
|
+
activeProjectName.value = null;
|
|
219
|
+
syncedSnapshotRef.value = null;
|
|
153
220
|
}
|
|
154
221
|
|
|
222
|
+
let bootstrapped = false;
|
|
223
|
+
let appliedWorkspaceKey: string | null = null;
|
|
224
|
+
|
|
225
|
+
function bootstrapInitialWorkspace() {
|
|
226
|
+
const initialWorkspace = toValue(options.initialWorkspace) ?? null;
|
|
227
|
+
if (!bootstrapped) {
|
|
228
|
+
bootstrapped = true;
|
|
229
|
+
if (hasWorkspacePayload(initialWorkspace)) {
|
|
230
|
+
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
231
|
+
appliedWorkspaceKey = workspaceApplyKey(initialWorkspace);
|
|
232
|
+
} else {
|
|
233
|
+
applyEmptyWorkspace();
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!hasWorkspacePayload(initialWorkspace)) return;
|
|
238
|
+
const key = workspaceApplyKey(initialWorkspace);
|
|
239
|
+
if (appliedWorkspaceKey === key) return;
|
|
240
|
+
appliedWorkspaceKey = key;
|
|
241
|
+
applyProjectRecord(initialWorkspace, { notifyApplied: false });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
onMounted(() => {
|
|
245
|
+
bootstrapInitialWorkspace();
|
|
246
|
+
void refreshProjects();
|
|
247
|
+
previewPollTimer = window.setInterval(refreshPreviewing, 2000);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
watch(
|
|
251
|
+
() => {
|
|
252
|
+
const record = toValue(options.initialWorkspace);
|
|
253
|
+
return record ? workspaceApplyKey(record) : "";
|
|
254
|
+
},
|
|
255
|
+
() => {
|
|
256
|
+
bootstrapInitialWorkspace();
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
155
260
|
async function persistProject(persistOptions: {
|
|
156
261
|
id?: string;
|
|
157
262
|
name: string;
|
|
@@ -161,7 +266,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
161
266
|
const now = Date.now();
|
|
162
267
|
const id = persistOptions.id ?? createWorkspaceProjectId();
|
|
163
268
|
const createdAt = persistOptions.createdAt ?? now;
|
|
164
|
-
const name = persistOptions.name.trim() || resolveProjectName();
|
|
269
|
+
const name = (persistOptions.name ?? "").trim() || resolveProjectName();
|
|
165
270
|
|
|
166
271
|
const record: WorkspaceProjectRecord = {
|
|
167
272
|
id,
|
|
@@ -171,13 +276,14 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
171
276
|
panelState: options.exportPanelData(),
|
|
172
277
|
blueprintDocument: snapshot.blueprintDocument,
|
|
173
278
|
blueprintMeta: snapshot.blueprintMeta,
|
|
174
|
-
productName:
|
|
279
|
+
productName:
|
|
280
|
+
typeof snapshot.productName === "string" ? snapshot.productName : resolveProjectName(),
|
|
175
281
|
titleIconDataUrl: snapshot.titleIconDataUrl || undefined,
|
|
176
282
|
};
|
|
177
283
|
|
|
178
|
-
await putWorkspaceProject(record);
|
|
179
|
-
writeWorkspacePreviewCache(record);
|
|
180
|
-
broadcastWorkspaceProjectUpdate(id, now);
|
|
284
|
+
await putWorkspaceProject(record, nameSpace());
|
|
285
|
+
writeWorkspacePreviewCache(record, nameSpace());
|
|
286
|
+
broadcastWorkspaceProjectUpdate(id, now, nameSpace());
|
|
181
287
|
activeProjectId.value = id;
|
|
182
288
|
activeProjectName.value = name;
|
|
183
289
|
syncedSnapshotRef.value = snapshot;
|
|
@@ -191,6 +297,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
191
297
|
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
192
298
|
const name = resolveProjectName();
|
|
193
299
|
const record = await persistProject({ name });
|
|
300
|
+
emitAbuilderEvent(AbuilderEvents.workspaceAdd, record);
|
|
194
301
|
return { name, id: record.id };
|
|
195
302
|
});
|
|
196
303
|
}
|
|
@@ -201,7 +308,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
201
308
|
return handleCreateProject();
|
|
202
309
|
}
|
|
203
310
|
return runBusyTask(t("common.savingWorkspace"), async () => {
|
|
204
|
-
const existing = await getWorkspaceProject(projectId);
|
|
311
|
+
const existing = await getWorkspaceProject(projectId, nameSpace());
|
|
205
312
|
const name = resolveProjectName();
|
|
206
313
|
const record = await persistProject({
|
|
207
314
|
id: projectId,
|
|
@@ -214,7 +321,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
214
321
|
|
|
215
322
|
async function handleOpenProject(id: string) {
|
|
216
323
|
await runBusyTask(t("common.loadingWorkspace"), async () => {
|
|
217
|
-
const record = await getWorkspaceProject(id);
|
|
324
|
+
const record = await getWorkspaceProject(id, nameSpace());
|
|
218
325
|
if (!record) {
|
|
219
326
|
await refreshProjects();
|
|
220
327
|
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
@@ -228,7 +335,7 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
228
335
|
if (!activeProjectId.value) {
|
|
229
336
|
throw new Error(t("panel.messages.saveWorkspaceFirst"));
|
|
230
337
|
}
|
|
231
|
-
const existing = await getWorkspaceProject(activeProjectId.value);
|
|
338
|
+
const existing = await getWorkspaceProject(activeProjectId.value, nameSpace());
|
|
232
339
|
if (!existing) {
|
|
233
340
|
activeProjectId.value = null;
|
|
234
341
|
activeProjectName.value = null;
|
|
@@ -237,18 +344,19 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
237
344
|
throw new Error(t("panel.messages.workspaceNotFound"));
|
|
238
345
|
}
|
|
239
346
|
const name = resolveProjectName() || existing.name;
|
|
240
|
-
await persistProject({
|
|
347
|
+
const record = await persistProject({
|
|
241
348
|
id: activeProjectId.value,
|
|
242
349
|
name,
|
|
243
350
|
createdAt: existing.createdAt,
|
|
244
351
|
});
|
|
352
|
+
emitAbuilderEvent(AbuilderEvents.workspaceSync, record);
|
|
245
353
|
return name;
|
|
246
354
|
});
|
|
247
355
|
}
|
|
248
356
|
|
|
249
357
|
async function handleDeleteProject(id: string) {
|
|
250
358
|
await runBusyTask(t("common.deletingWorkspace"), async () => {
|
|
251
|
-
await deleteWorkspaceProject(id);
|
|
359
|
+
await deleteWorkspaceProject(id, nameSpace());
|
|
252
360
|
if (activeProjectId.value === id) {
|
|
253
361
|
activeProjectId.value = null;
|
|
254
362
|
activeProjectName.value = null;
|
|
@@ -266,9 +374,10 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
266
374
|
await handleSyncProject();
|
|
267
375
|
}
|
|
268
376
|
const record =
|
|
269
|
-
(await getWorkspaceProject(id)) ??
|
|
377
|
+
(await getWorkspaceProject(id, nameSpace())) ??
|
|
378
|
+
readWorkspacePreviewCache(id, nameSpace());
|
|
270
379
|
if (record) {
|
|
271
|
-
writeWorkspacePreviewCache(record);
|
|
380
|
+
writeWorkspacePreviewCache(record, nameSpace());
|
|
272
381
|
}
|
|
273
382
|
const existing = previewWindows.get(id);
|
|
274
383
|
if (existing && !existing.closed) {
|
|
@@ -280,9 +389,10 @@ export function useWorkspaceProjects(options: UseWorkspaceProjectsOptions) {
|
|
|
280
389
|
refreshPreviewing();
|
|
281
390
|
return;
|
|
282
391
|
}
|
|
392
|
+
const ns = nameSpace();
|
|
283
393
|
const win = window.open(
|
|
284
|
-
buildOnlinePreviewUrl(id),
|
|
285
|
-
|
|
394
|
+
buildOnlinePreviewUrl(id, ns),
|
|
395
|
+
previewWindowName(id, ns)
|
|
286
396
|
);
|
|
287
397
|
if (win) {
|
|
288
398
|
previewWindows.set(id, win);
|