@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.
@@ -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
- export type WorkspaceSyncEventPayload = WorkspaceData;
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
- set.add(callback as EventCallback);
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(callback as EventCallback);
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 (!data || (data.type !== "deleted" && data.type !== "cleared")) return;
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 { broadcastWorkspaceProjectsChanged } from "./workspace-project-sync";
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: normalizeAppNameSpace(nameSpace) || null,
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: normalizeAppNameSpace(nameSpace) || null,
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: 2px solid #fff !important;
108
+ border-color: #fff !important;
48
109
  background: #1990ff !important;
49
110
  background: var(--moveable-color) !important;
50
111
  z-index: 10 !important;