@arronqzy/vue-view 0.1.24 → 0.1.26
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 +1 -11
- package/package.json +3 -3
- package/src/index.ts +25 -1
- package/src/panel/VueViewOnlinePreview.vue +25 -0
- package/src/panel/VueViewPanel.vue +188 -63
- package/src/panel/components/ElementsLayer.vue +1 -1
- package/src/panel/components/MaterialPreview.vue +28 -27
- package/src/panel/components/MaterialSidebar.vue +288 -225
- package/src/panel/components/MaterialSidebarTreeNode.vue +15 -10
- package/src/panel/components/MoveableLayer.vue +8 -0
- package/src/panel/components/PanelCanvas.vue +1 -0
- package/src/panel/components/PanelConfigSidebar.vue +23 -41
- package/src/panel/components/PanelRulers.vue +158 -40
- package/src/panel/components/SelectLayer.vue +2 -1
- package/src/panel/components/config/ConfigFieldGroup.vue +3 -3
- package/src/panel/components/config/ConfigSection.vue +5 -5
- package/src/panel/hooks/useWorkspaceProjects.ts +209 -67
- package/src/panel/lib/panel-ruler-canvas.ts +8 -4
- package/src/panel/library/abuilder-storage.ts +101 -3
- package/src/panel/library/abuilder-view-handle.ts +100 -0
- package/src/panel/library/capture-preview-snapshot.ts +800 -0
- package/src/panel/library/event-subscription.ts +70 -5
- package/src/panel/library/preview-snapshot.ts +252 -0
- package/src/panel/library/workspace-project-sync.ts +7 -1
- package/src/panel/library/workspace-storage.ts +58 -4
- package/src/tailwind.css +66 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeAppNameSpace } from "@arronqzy/blueprint-dsl";
|
|
1
2
|
import type { WorkspaceProjectRecord } from "./workspace-project-db";
|
|
2
3
|
|
|
3
4
|
export const AbuilderEvents = {
|
|
@@ -7,10 +8,14 @@ export const AbuilderEvents = {
|
|
|
7
8
|
workspaceSync: "workspace:sync",
|
|
8
9
|
/** 删除工作区(界面删除,或调用 deleteWorkspaceProject / clearWorkspaceProjects) */
|
|
9
10
|
workspaceDelete: "workspace:delete",
|
|
11
|
+
/** 蓝图库写入 / 删除 / 清空 */
|
|
12
|
+
blueprintLibraryChanged: "blueprint-library:changed",
|
|
10
13
|
/** 开始上传接口集合 */
|
|
11
14
|
apiCollectionUploadStart: "api-collection:upload-start",
|
|
12
15
|
/** 上传接口集合成功 */
|
|
13
16
|
apiCollectionUploadSuccess: "api-collection:upload-success",
|
|
17
|
+
/** 接口集合写入 / 删除 / 清空 */
|
|
18
|
+
apiCollectionChanged: "api-collection:changed",
|
|
14
19
|
} as const;
|
|
15
20
|
|
|
16
21
|
export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
|
|
@@ -18,8 +23,14 @@ export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEve
|
|
|
18
23
|
/** 工作区完整数据(面板 + 蓝图),可用于外部持久化与再次加载 */
|
|
19
24
|
export type WorkspaceData = WorkspaceProjectRecord;
|
|
20
25
|
|
|
21
|
-
export type WorkspaceAddEventPayload = WorkspaceData
|
|
22
|
-
|
|
26
|
+
export type WorkspaceAddEventPayload = WorkspaceData & {
|
|
27
|
+
/** 所属 nameSpace,默认库为 null */
|
|
28
|
+
nameSpace: string | null;
|
|
29
|
+
};
|
|
30
|
+
export type WorkspaceSyncEventPayload = WorkspaceData & {
|
|
31
|
+
/** 所属 nameSpace,默认库为 null */
|
|
32
|
+
nameSpace: string | null;
|
|
33
|
+
};
|
|
23
34
|
export type WorkspaceDeleteEventPayload = {
|
|
24
35
|
/** 被删除的工作区 id */
|
|
25
36
|
ids: string[];
|
|
@@ -27,9 +38,17 @@ export type WorkspaceDeleteEventPayload = {
|
|
|
27
38
|
nameSpace: string | null;
|
|
28
39
|
};
|
|
29
40
|
|
|
41
|
+
export type BlueprintLibraryChangedEventPayload = {
|
|
42
|
+
nameSpace: string | null;
|
|
43
|
+
action: "put" | "delete" | "clear";
|
|
44
|
+
/** 涉及的蓝图 id;clear 时可能为空 */
|
|
45
|
+
ids?: string[];
|
|
46
|
+
};
|
|
47
|
+
|
|
30
48
|
export type ApiCollectionUploadStartEventPayload = {
|
|
31
49
|
fileNames: string[];
|
|
32
50
|
count: number;
|
|
51
|
+
nameSpace: string | null;
|
|
33
52
|
};
|
|
34
53
|
|
|
35
54
|
export type ApiCollectionUploadSuccessEventPayload = {
|
|
@@ -39,20 +58,57 @@ export type ApiCollectionUploadSuccessEventPayload = {
|
|
|
39
58
|
apiCount: number;
|
|
40
59
|
persistLocal: boolean;
|
|
41
60
|
}>;
|
|
61
|
+
nameSpace: string | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ApiCollectionChangedEventPayload = {
|
|
65
|
+
nameSpace: string | null;
|
|
66
|
+
action: "put" | "delete" | "clear";
|
|
67
|
+
ids?: string[];
|
|
42
68
|
};
|
|
43
69
|
|
|
44
70
|
export type AbuilderEventPayloadMap = {
|
|
45
71
|
[AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
|
|
46
72
|
[AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
|
|
47
73
|
[AbuilderEvents.workspaceDelete]: WorkspaceDeleteEventPayload;
|
|
74
|
+
[AbuilderEvents.blueprintLibraryChanged]: BlueprintLibraryChangedEventPayload;
|
|
48
75
|
[AbuilderEvents.apiCollectionUploadStart]: ApiCollectionUploadStartEventPayload;
|
|
49
76
|
[AbuilderEvents.apiCollectionUploadSuccess]: ApiCollectionUploadSuccessEventPayload;
|
|
77
|
+
[AbuilderEvents.apiCollectionChanged]: ApiCollectionChangedEventPayload;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type AddEventSubscriptionOptions = {
|
|
81
|
+
/**
|
|
82
|
+
* 仅接收该 nameSpace 的事件。
|
|
83
|
+
* 省略则接收全部;传入空字符串 / null 表示默认库。
|
|
84
|
+
*/
|
|
85
|
+
nameSpace?: string | null;
|
|
50
86
|
};
|
|
51
87
|
|
|
52
88
|
type EventCallback = (payload: unknown) => void;
|
|
53
89
|
|
|
54
90
|
const listeners = new Map<string, Set<EventCallback>>();
|
|
55
91
|
|
|
92
|
+
function resolvePayloadNameSpace(payload: unknown): string | null | undefined {
|
|
93
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
94
|
+
if (!("nameSpace" in payload)) return undefined;
|
|
95
|
+
const value = (payload as { nameSpace?: unknown }).nameSpace;
|
|
96
|
+
if (value === null || value === undefined) return null;
|
|
97
|
+
if (typeof value === "string") return value;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function matchesNameSpaceFilter(
|
|
102
|
+
payload: unknown,
|
|
103
|
+
filter: string | null | undefined
|
|
104
|
+
): boolean {
|
|
105
|
+
if (filter === undefined) return true;
|
|
106
|
+
const wanted = normalizeAppNameSpace(filter);
|
|
107
|
+
const actual = resolvePayloadNameSpace(payload);
|
|
108
|
+
if (actual === undefined) return true;
|
|
109
|
+
return normalizeAppNameSpace(actual) === wanted;
|
|
110
|
+
}
|
|
111
|
+
|
|
56
112
|
export function emitAbuilderEvent<T extends AbuilderEventName>(
|
|
57
113
|
eventName: T,
|
|
58
114
|
payload: AbuilderEventPayloadMap[T]
|
|
@@ -70,17 +126,26 @@ export function emitAbuilderEvent<T extends AbuilderEventName>(
|
|
|
70
126
|
|
|
71
127
|
export function addEventSubscription<T extends AbuilderEventName>(
|
|
72
128
|
eventName: T,
|
|
73
|
-
callback: (payload: AbuilderEventPayloadMap[T]) => void
|
|
129
|
+
callback: (payload: AbuilderEventPayloadMap[T]) => void,
|
|
130
|
+
options?: AddEventSubscriptionOptions
|
|
74
131
|
): { unsubscribe: () => void } {
|
|
75
132
|
let set = listeners.get(eventName);
|
|
76
133
|
if (!set) {
|
|
77
134
|
set = new Set();
|
|
78
135
|
listeners.set(eventName, set);
|
|
79
136
|
}
|
|
80
|
-
|
|
137
|
+
const filter =
|
|
138
|
+
options && Object.prototype.hasOwnProperty.call(options, "nameSpace")
|
|
139
|
+
? options.nameSpace
|
|
140
|
+
: undefined;
|
|
141
|
+
const wrapped: EventCallback = (payload) => {
|
|
142
|
+
if (!matchesNameSpaceFilter(payload, filter)) return;
|
|
143
|
+
callback(payload as AbuilderEventPayloadMap[T]);
|
|
144
|
+
};
|
|
145
|
+
set.add(wrapped);
|
|
81
146
|
return {
|
|
82
147
|
unsubscribe: () => {
|
|
83
|
-
set.delete(
|
|
148
|
+
set.delete(wrapped);
|
|
84
149
|
if (set.size === 0) {
|
|
85
150
|
listeners.delete(eventName);
|
|
86
151
|
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { PanelElement, PanelLayer } from "../types";
|
|
2
|
+
import { CHART_TYPES, buildChartOption } from "../utils/chartOptionBuilder";
|
|
3
|
+
import {
|
|
4
|
+
computePanelSceneBounds,
|
|
5
|
+
resolvePreviewLayerElements,
|
|
6
|
+
} from "../utils/panelStateIO";
|
|
7
|
+
import {
|
|
8
|
+
capturePreviewLayerSnapshot,
|
|
9
|
+
capturePreviewSceneElement,
|
|
10
|
+
scaleDataUrl,
|
|
11
|
+
type CapturePreviewSceneOptions,
|
|
12
|
+
} from "./capture-preview-snapshot";
|
|
13
|
+
|
|
14
|
+
export type GetPreviewSnapshotOptions = CapturePreviewSceneOptions;
|
|
15
|
+
|
|
16
|
+
type PreviewSnapshotProvider = () => Promise<string | null>;
|
|
17
|
+
|
|
18
|
+
let previewSnapshotProvider: PreviewSnapshotProvider | null = null;
|
|
19
|
+
|
|
20
|
+
export function registerPreviewSnapshotProvider(provider: PreviewSnapshotProvider | null) {
|
|
21
|
+
previewSnapshotProvider = provider;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function serializeNodeDom(
|
|
25
|
+
sourceNode: HTMLElement,
|
|
26
|
+
sourceElement: PanelElement,
|
|
27
|
+
minX: number,
|
|
28
|
+
minY: number,
|
|
29
|
+
isChartNode: boolean
|
|
30
|
+
): string {
|
|
31
|
+
const clone = sourceNode.cloneNode(true) as HTMLElement;
|
|
32
|
+
clone.className = clone.className
|
|
33
|
+
.split(/\s+/)
|
|
34
|
+
.filter((cls) => cls && !cls.startsWith("ring-"))
|
|
35
|
+
.join(" ");
|
|
36
|
+
clone.removeAttribute("data-moveable-target");
|
|
37
|
+
clone.style.left = `${sourceElement.x - minX}px`;
|
|
38
|
+
clone.style.top = `${sourceElement.y - minY}px`;
|
|
39
|
+
const sourceCanvases = Array.from(sourceNode.querySelectorAll("canvas"));
|
|
40
|
+
const cloneCanvases = Array.from(clone.querySelectorAll("canvas"));
|
|
41
|
+
if (isChartNode) return clone.outerHTML;
|
|
42
|
+
sourceCanvases.forEach((srcCanvas, idx) => {
|
|
43
|
+
const clonedCanvas = cloneCanvases[idx];
|
|
44
|
+
if (!clonedCanvas) return;
|
|
45
|
+
try {
|
|
46
|
+
const dataUrl = srcCanvas.toDataURL("image/png");
|
|
47
|
+
const img = document.createElement("img");
|
|
48
|
+
img.src = dataUrl;
|
|
49
|
+
img.style.width = "100%";
|
|
50
|
+
img.style.height = "100%";
|
|
51
|
+
img.style.objectFit = "fill";
|
|
52
|
+
img.style.display = "block";
|
|
53
|
+
clonedCanvas.replaceWith(img);
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore tainted canvas
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return clone.outerHTML;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function waitFrames(count = 2): Promise<void> {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
let remaining = count;
|
|
64
|
+
const step = () => {
|
|
65
|
+
remaining -= 1;
|
|
66
|
+
if (remaining <= 0) resolve();
|
|
67
|
+
else requestAnimationFrame(step);
|
|
68
|
+
};
|
|
69
|
+
requestAnimationFrame(step);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type BuildEditorPreviewSnapshotSceneOptions = {
|
|
74
|
+
canvasRoot: HTMLElement | null;
|
|
75
|
+
allElements: PanelElement[];
|
|
76
|
+
layers: PanelLayer[];
|
|
77
|
+
activeLayerId: string;
|
|
78
|
+
outputScale: boolean;
|
|
79
|
+
retryCount?: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export async function buildEditorPreviewSnapshotScene(
|
|
83
|
+
options: BuildEditorPreviewSnapshotSceneOptions
|
|
84
|
+
): Promise<{ scene: HTMLElement; width: number; height: number } | null> {
|
|
85
|
+
const retryCount = options.retryCount ?? 0;
|
|
86
|
+
const layerElements = resolvePreviewLayerElements(
|
|
87
|
+
options.allElements,
|
|
88
|
+
options.layers,
|
|
89
|
+
options.activeLayerId
|
|
90
|
+
);
|
|
91
|
+
if (!layerElements.length) return null;
|
|
92
|
+
|
|
93
|
+
const sceneBounds = computePanelSceneBounds(layerElements);
|
|
94
|
+
const mountedNodes = layerElements.map((el) => ({
|
|
95
|
+
el,
|
|
96
|
+
node:
|
|
97
|
+
options.canvasRoot?.querySelector<HTMLElement>(`[data-element-id="${el.id}"]`) ??
|
|
98
|
+
null,
|
|
99
|
+
}));
|
|
100
|
+
const mountedCount = mountedNodes.filter((item) => !!item.node).length;
|
|
101
|
+
if (mountedCount < layerElements.length && retryCount < 20) {
|
|
102
|
+
await new Promise((resolve) => window.setTimeout(resolve, 80));
|
|
103
|
+
return buildEditorPreviewSnapshotScene({ ...options, retryCount: retryCount + 1 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const previewNodesHtml = mountedNodes
|
|
107
|
+
.map((item) => {
|
|
108
|
+
if (!item.node) return "";
|
|
109
|
+
return serializeNodeDom(
|
|
110
|
+
item.node,
|
|
111
|
+
item.el,
|
|
112
|
+
sceneBounds.minX,
|
|
113
|
+
sceneBounds.minY,
|
|
114
|
+
CHART_TYPES.has(item.el.materialType ?? "")
|
|
115
|
+
);
|
|
116
|
+
})
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join("");
|
|
119
|
+
if (!previewNodesHtml && retryCount < 20) {
|
|
120
|
+
await new Promise((resolve) => window.setTimeout(resolve, 80));
|
|
121
|
+
return buildEditorPreviewSnapshotScene({ ...options, retryCount: retryCount + 1 });
|
|
122
|
+
}
|
|
123
|
+
if (!previewNodesHtml) return null;
|
|
124
|
+
|
|
125
|
+
const host = document.createElement("div");
|
|
126
|
+
host.setAttribute("data-preview-snapshot-host", "true");
|
|
127
|
+
host.style.position = "fixed";
|
|
128
|
+
host.style.left = "-100000px";
|
|
129
|
+
host.style.top = "0";
|
|
130
|
+
host.style.width = `${sceneBounds.width}px`;
|
|
131
|
+
host.style.height = `${sceneBounds.height}px`;
|
|
132
|
+
host.style.overflow = "hidden";
|
|
133
|
+
host.style.pointerEvents = "none";
|
|
134
|
+
host.style.background = "#ffffff";
|
|
135
|
+
|
|
136
|
+
const scene = document.createElement("div");
|
|
137
|
+
scene.id = "preview-scene";
|
|
138
|
+
scene.className = "relative shrink-0 origin-top-left";
|
|
139
|
+
scene.style.width = `${sceneBounds.width}px`;
|
|
140
|
+
scene.style.height = `${sceneBounds.height}px`;
|
|
141
|
+
scene.style.transformOrigin = "left top";
|
|
142
|
+
scene.innerHTML = previewNodesHtml;
|
|
143
|
+
host.appendChild(scene);
|
|
144
|
+
document.body.appendChild(host);
|
|
145
|
+
|
|
146
|
+
const gridNodeIds = layerElements
|
|
147
|
+
.filter((el) => el.materialType === "grid")
|
|
148
|
+
.map((el) => el.id);
|
|
149
|
+
for (const id of gridNodeIds) {
|
|
150
|
+
const gridNode = scene.querySelector<HTMLElement>(`[data-element-id="${id}"]`);
|
|
151
|
+
const content = gridNode?.firstElementChild as HTMLElement | null;
|
|
152
|
+
if (content) {
|
|
153
|
+
content.innerHTML = "";
|
|
154
|
+
content.removeAttribute("style");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const chartNodes = layerElements.filter((el) =>
|
|
159
|
+
CHART_TYPES.has(el.materialType ?? "")
|
|
160
|
+
);
|
|
161
|
+
for (const el of chartNodes) {
|
|
162
|
+
const hostEl = scene.querySelector<HTMLElement>(`[data-element-id="${el.id}"] > div`);
|
|
163
|
+
if (!hostEl) continue;
|
|
164
|
+
hostEl.innerHTML = "";
|
|
165
|
+
const chart = (
|
|
166
|
+
await import("echarts")
|
|
167
|
+
).init(hostEl, null, { renderer: el.chart?.renderMode ?? "canvas" });
|
|
168
|
+
chart.setOption(buildChartOption(el) ?? {}, true);
|
|
169
|
+
chart.resize();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await waitFrames(2);
|
|
173
|
+
return { scene, width: sceneBounds.width, height: sceneBounds.height };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function disposePreviewSnapshotScene(scene: HTMLElement) {
|
|
177
|
+
const host = scene.parentElement;
|
|
178
|
+
host?.remove();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function finalizePreviewSnapshot(
|
|
182
|
+
dataUrl: string,
|
|
183
|
+
options?: GetPreviewSnapshotOptions
|
|
184
|
+
): Promise<string> {
|
|
185
|
+
if (!options) return dataUrl;
|
|
186
|
+
const mimeType = options.mimeType ?? "image/png";
|
|
187
|
+
const quality = options.quality ?? 0.92;
|
|
188
|
+
if (options.maxWidth || options.maxHeight) {
|
|
189
|
+
return scaleDataUrl(dataUrl, options.maxWidth, options.maxHeight, mimeType, quality);
|
|
190
|
+
}
|
|
191
|
+
if (mimeType === "image/jpeg" && !dataUrl.startsWith("data:image/jpeg")) {
|
|
192
|
+
return scaleDataUrl(dataUrl, undefined, undefined, mimeType, quality);
|
|
193
|
+
}
|
|
194
|
+
return dataUrl;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 获取当前预览画面的 base64 图片(data URL),可用于工作区缩略图等场景。
|
|
199
|
+
* 优先使用编辑器或在线预览页注册的截图提供者,其次尝试页面上的 `#preview-scene`。
|
|
200
|
+
*/
|
|
201
|
+
export async function getPreviewSnapshot(
|
|
202
|
+
options?: GetPreviewSnapshotOptions
|
|
203
|
+
): Promise<string> {
|
|
204
|
+
let dataUrl: string | null = null;
|
|
205
|
+
if (previewSnapshotProvider) {
|
|
206
|
+
dataUrl = await previewSnapshotProvider();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!dataUrl) {
|
|
210
|
+
const scene = document.getElementById("preview-scene");
|
|
211
|
+
if (scene instanceof HTMLElement) {
|
|
212
|
+
const width = scene.offsetWidth || Number.parseFloat(scene.style.width) || 1;
|
|
213
|
+
const height = scene.offsetHeight || Number.parseFloat(scene.style.height) || 1;
|
|
214
|
+
return capturePreviewSceneElement(scene, width, height, options);
|
|
215
|
+
}
|
|
216
|
+
throw new Error(
|
|
217
|
+
"Preview snapshot is unavailable. Mount the editor or open online preview first."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return finalizePreviewSnapshot(dataUrl, options);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function captureEditorPreviewSnapshot(
|
|
225
|
+
options: BuildEditorPreviewSnapshotSceneOptions & GetPreviewSnapshotOptions
|
|
226
|
+
): Promise<string | null> {
|
|
227
|
+
const retryCount = options.retryCount ?? 0;
|
|
228
|
+
const layerElements = resolvePreviewLayerElements(
|
|
229
|
+
options.allElements,
|
|
230
|
+
options.layers,
|
|
231
|
+
options.activeLayerId
|
|
232
|
+
);
|
|
233
|
+
if (!layerElements.length) return null;
|
|
234
|
+
|
|
235
|
+
const mountedCount = layerElements.filter((el) =>
|
|
236
|
+
Boolean(options.canvasRoot?.querySelector(`[data-element-id="${el.id}"]`))
|
|
237
|
+
).length;
|
|
238
|
+
if (mountedCount < layerElements.length && retryCount < 20) {
|
|
239
|
+
await new Promise((resolve) => window.setTimeout(resolve, 80));
|
|
240
|
+
return captureEditorPreviewSnapshot({ ...options, retryCount: retryCount + 1 });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return capturePreviewLayerSnapshot({
|
|
244
|
+
canvasRoot: options.canvasRoot,
|
|
245
|
+
elements: layerElements,
|
|
246
|
+
backgroundColor: options.backgroundColor,
|
|
247
|
+
maxWidth: options.maxWidth,
|
|
248
|
+
maxHeight: options.maxHeight,
|
|
249
|
+
mimeType: options.mimeType,
|
|
250
|
+
quality: options.quality,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -11,6 +11,7 @@ export type WorkspaceProjectSyncMessage = {
|
|
|
11
11
|
|
|
12
12
|
export type WorkspaceProjectsChangedMessage =
|
|
13
13
|
| { type: "deleted"; projectIds: string[] }
|
|
14
|
+
| { type: "upserted"; projectIds: string[] }
|
|
14
15
|
| { type: "cleared" };
|
|
15
16
|
|
|
16
17
|
export function broadcastWorkspaceProjectsChanged(
|
|
@@ -33,7 +34,12 @@ export function subscribeWorkspaceProjectsChanged(
|
|
|
33
34
|
const channel = new BroadcastChannel(appStorageKey(LIST_CHANNEL, nameSpace));
|
|
34
35
|
channel.onmessage = (event: MessageEvent<WorkspaceProjectsChangedMessage>) => {
|
|
35
36
|
const data = event.data;
|
|
36
|
-
if (
|
|
37
|
+
if (
|
|
38
|
+
!data ||
|
|
39
|
+
(data.type !== "deleted" && data.type !== "cleared" && data.type !== "upserted")
|
|
40
|
+
) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
37
43
|
onChange(data);
|
|
38
44
|
};
|
|
39
45
|
return () => channel.close();
|
|
@@ -4,16 +4,70 @@ import {
|
|
|
4
4
|
deleteWorkspaceProject as deleteWorkspaceProjectRecord,
|
|
5
5
|
getWorkspaceProject,
|
|
6
6
|
listWorkspaceProjects,
|
|
7
|
+
putWorkspaceProject as putWorkspaceProjectRecord,
|
|
8
|
+
type WorkspaceProjectRecord,
|
|
7
9
|
} from "./workspace-project-db";
|
|
8
10
|
import {
|
|
9
11
|
clearWorkspacePreviewCaches,
|
|
10
12
|
removeWorkspacePreviewCache,
|
|
13
|
+
writeWorkspacePreviewCache,
|
|
11
14
|
} from "./workspace-project-cache";
|
|
12
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
broadcastWorkspaceProjectUpdate,
|
|
17
|
+
broadcastWorkspaceProjectsChanged,
|
|
18
|
+
} from "./workspace-project-sync";
|
|
13
19
|
import { AbuilderEvents, emitAbuilderEvent } from "./event-subscription";
|
|
14
20
|
|
|
15
21
|
export { listWorkspaceProjects, getWorkspaceProject };
|
|
16
|
-
export type { WorkspaceProjectListItem } from "./workspace-project-db";
|
|
22
|
+
export type { WorkspaceProjectListItem, WorkspaceProjectRecord } from "./workspace-project-db";
|
|
23
|
+
|
|
24
|
+
export type PutWorkspaceProjectOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* 写入后是否发出工作区事件。
|
|
27
|
+
* - `false` / 省略:只写 IndexedDB(宿主自己管后端时常用)
|
|
28
|
+
* - `"add"`:发出 `workspace:add`
|
|
29
|
+
* - `"sync"`:发出 `workspace:sync`
|
|
30
|
+
*/
|
|
31
|
+
emitEvent?: false | "add" | "sync";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function resolvedNameSpace(nameSpace?: string | null): string | null {
|
|
35
|
+
return normalizeAppNameSpace(nameSpace) || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 写入 / 覆盖 IndexedDB 中的工作区(upsert),并更新预览缓存、通知其他标签页。
|
|
40
|
+
* `nameSpace` 需与面板的 `nameSpace` 一致。
|
|
41
|
+
*/
|
|
42
|
+
export async function putWorkspaceProject(
|
|
43
|
+
record: WorkspaceProjectRecord,
|
|
44
|
+
nameSpace?: string | null,
|
|
45
|
+
options?: PutWorkspaceProjectOptions
|
|
46
|
+
): Promise<WorkspaceProjectRecord> {
|
|
47
|
+
const saved = await putWorkspaceProjectRecord(record, nameSpace);
|
|
48
|
+
writeWorkspacePreviewCache(saved, nameSpace);
|
|
49
|
+
broadcastWorkspaceProjectUpdate(saved.id, saved.updatedAt, nameSpace);
|
|
50
|
+
broadcastWorkspaceProjectsChanged(
|
|
51
|
+
{ type: "upserted", projectIds: [saved.id] },
|
|
52
|
+
nameSpace
|
|
53
|
+
);
|
|
54
|
+
const ns = resolvedNameSpace(nameSpace);
|
|
55
|
+
if (options?.emitEvent === "add") {
|
|
56
|
+
emitAbuilderEvent(AbuilderEvents.workspaceAdd, { ...saved, nameSpace: ns });
|
|
57
|
+
} else if (options?.emitEvent === "sync") {
|
|
58
|
+
emitAbuilderEvent(AbuilderEvents.workspaceSync, { ...saved, nameSpace: ns });
|
|
59
|
+
}
|
|
60
|
+
return saved;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `putWorkspaceProject` 的别名 */
|
|
64
|
+
export async function upsertWorkspaceProject(
|
|
65
|
+
record: WorkspaceProjectRecord,
|
|
66
|
+
nameSpace?: string | null,
|
|
67
|
+
options?: PutWorkspaceProjectOptions
|
|
68
|
+
): Promise<WorkspaceProjectRecord> {
|
|
69
|
+
return putWorkspaceProject(record, nameSpace, options);
|
|
70
|
+
}
|
|
17
71
|
|
|
18
72
|
/**
|
|
19
73
|
* 删除 IndexedDB 中保存的单个工作区(及其 localStorage 预览标记)。
|
|
@@ -28,7 +82,7 @@ export async function deleteWorkspaceProject(
|
|
|
28
82
|
broadcastWorkspaceProjectsChanged({ type: "deleted", projectIds: [id] }, nameSpace);
|
|
29
83
|
emitAbuilderEvent(AbuilderEvents.workspaceDelete, {
|
|
30
84
|
ids: [id],
|
|
31
|
-
nameSpace:
|
|
85
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
32
86
|
});
|
|
33
87
|
}
|
|
34
88
|
|
|
@@ -44,7 +98,7 @@ export async function clearWorkspaceProjects(nameSpace?: string | null): Promise
|
|
|
44
98
|
if (items.length > 0) {
|
|
45
99
|
emitAbuilderEvent(AbuilderEvents.workspaceDelete, {
|
|
46
100
|
ids: items.map((item) => item.id),
|
|
47
|
-
nameSpace:
|
|
101
|
+
nameSpace: resolvedNameSpace(nameSpace),
|
|
48
102
|
});
|
|
49
103
|
}
|
|
50
104
|
return items.length;
|
package/src/tailwind.css
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 物料侧栏:容器查询写在源 CSS 里(JIT 的 @[Npx] 经常未进 dist)
|
|
3
|
+
* 窄:单列、卡片最多 112px;宽:auto-fill 换行,单卡约 88–104px
|
|
4
|
+
*/
|
|
5
|
+
.rv-material-sidebar {
|
|
6
|
+
container-type: inline-size;
|
|
7
|
+
container-name: rv-material;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.rv-material-grid {
|
|
11
|
+
display: grid;
|
|
12
|
+
grid-template-columns: minmax(0, 1fr);
|
|
13
|
+
justify-items: stretch;
|
|
14
|
+
gap: 0.375rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.rv-material-card {
|
|
18
|
+
width: 100%;
|
|
19
|
+
max-width: 112px;
|
|
20
|
+
justify-self: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@container rv-material (min-width: 200px) {
|
|
24
|
+
.rv-material-grid {
|
|
25
|
+
grid-template-columns: repeat(auto-fill, minmax(88px, 104px));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.rv-material-card {
|
|
29
|
+
max-width: none;
|
|
30
|
+
justify-self: stretch;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* 配置侧栏:窄时多列表单改单列,避免挤成一团
|
|
36
|
+
*/
|
|
37
|
+
.scope-config-sidebar {
|
|
38
|
+
container-type: inline-size;
|
|
39
|
+
container-name: rv-config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@container rv-config (max-width: 260px) {
|
|
43
|
+
.scope-config-sidebar .grid {
|
|
44
|
+
grid-template-columns: minmax(0, 1fr) !important;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.scope-config-sidebar .grid > * {
|
|
48
|
+
min-width: 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* 节点选中描边:用倒数抵消画布 scale,屏幕上始终约 2px(勿用 ring/border,会随缩放变粗)
|
|
54
|
+
*/
|
|
55
|
+
.rv-node-selected {
|
|
56
|
+
box-shadow: 0 0 0 calc(2px / var(--rv-canvas-scale, 1)) rgb(59 130 246 / 0.9);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Selecto 框选:渲染在 body,线宽保持屏幕 1px */
|
|
60
|
+
.selecto-selection {
|
|
61
|
+
border-width: 1px !important;
|
|
62
|
+
box-sizing: border-box !important;
|
|
63
|
+
}
|
|
64
|
+
|
|
1
65
|
/*
|
|
2
66
|
* Moveable 控制框:
|
|
3
67
|
* - 根节点必须高于画布元素 zIndex(节点默认 ≥1),否则多选组框线会被节点盖住,只剩锚点可见
|
|
@@ -37,14 +101,11 @@
|
|
|
37
101
|
background: transparent !important;
|
|
38
102
|
}
|
|
39
103
|
|
|
104
|
+
/* 锚点尺寸交给 Moveable 的 scale(zoom) 补偿,勿写死 px 覆盖,否则放大后会偏粗 */
|
|
40
105
|
.rv-moveable-layer .moveable-control {
|
|
41
|
-
width: 14px !important;
|
|
42
|
-
height: 14px !important;
|
|
43
|
-
margin-top: -7px !important;
|
|
44
|
-
margin-left: -7px !important;
|
|
45
106
|
border-radius: 50% !important;
|
|
46
107
|
box-sizing: border-box !important;
|
|
47
|
-
border:
|
|
108
|
+
border-color: #fff !important;
|
|
48
109
|
background: #1990ff !important;
|
|
49
110
|
background: var(--moveable-color) !important;
|
|
50
111
|
z-index: 10 !important;
|