@arronqzy/vue-view 0.1.0

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.
Files changed (75) hide show
  1. package/README.md +50 -0
  2. package/package.json +49 -0
  3. package/src/env.d.ts +62 -0
  4. package/src/index.ts +4 -0
  5. package/src/panel/VueViewOnlinePreview.vue +276 -0
  6. package/src/panel/VueViewPanel.vue +871 -0
  7. package/src/panel/components/ConfigHintIcon.vue +34 -0
  8. package/src/panel/components/ElementsLayer.vue +165 -0
  9. package/src/panel/components/MaterialPreview.vue +135 -0
  10. package/src/panel/components/MaterialSidebar.vue +526 -0
  11. package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
  12. package/src/panel/components/MoveableLayer.vue +859 -0
  13. package/src/panel/components/PanelCanvas.vue +630 -0
  14. package/src/panel/components/PanelConfigSidebar.vue +397 -0
  15. package/src/panel/components/PanelRulers.vue +177 -0
  16. package/src/panel/components/SelectLayer.vue +115 -0
  17. package/src/panel/components/ViewElementScopePanel.vue +76 -0
  18. package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
  19. package/src/panel/components/WorkspaceProjectNav.vue +192 -0
  20. package/src/panel/components/WorkspaceStageSplit.vue +258 -0
  21. package/src/panel/components/config/ConfigColorField.vue +52 -0
  22. package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
  23. package/src/panel/components/config/ConfigSection.vue +50 -0
  24. package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
  25. package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
  26. package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
  27. package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
  28. package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
  29. package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
  30. package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
  31. package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
  32. package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
  33. package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
  34. package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
  35. package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
  36. package/src/panel/components/config/shared.ts +74 -0
  37. package/src/panel/components/elementsLayerNodes.ts +830 -0
  38. package/src/panel/components/materialSidebarData.ts +85 -0
  39. package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
  40. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
  41. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
  42. package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
  43. package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
  44. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
  45. package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
  46. package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
  47. package/src/panel/constants/messages.ts +34 -0
  48. package/src/panel/constants/zIndex.ts +6 -0
  49. package/src/panel/hooks/usePanelElements.ts +1075 -0
  50. package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
  51. package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
  52. package/src/panel/lib/panel-ruler-canvas.ts +139 -0
  53. package/src/panel/library/workspace-project-cache.ts +23 -0
  54. package/src/panel/library/workspace-project-db.ts +111 -0
  55. package/src/panel/library/workspace-project-sync.ts +41 -0
  56. package/src/panel/library/workspace-snapshot.ts +30 -0
  57. package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
  58. package/src/panel/scope/view-scope-store.ts +82 -0
  59. package/src/panel/types.ts +127 -0
  60. package/src/panel/utils/chartOptionBuilder.ts +327 -0
  61. package/src/panel/utils/gridPlacement.ts +189 -0
  62. package/src/panel/utils/mappingLayerOps.ts +142 -0
  63. package/src/panel/utils/panelElementDefaults.ts +161 -0
  64. package/src/panel/utils/panelElementNodes.ts +35 -0
  65. package/src/panel/utils/panelStateIO.ts +124 -0
  66. package/src/panel/utils/scope-autocomplete.ts +114 -0
  67. package/src/panel/utils/scope-field-labels.ts +46 -0
  68. package/src/panel/utils/scope-template-chart.ts +92 -0
  69. package/src/panel/utils/scope-template-preview.ts +124 -0
  70. package/src/panel/utils/scope-template-spread.ts +229 -0
  71. package/src/panel/utils/scope-template-warnings.ts +243 -0
  72. package/src/panel/utils/scope-template.ts +97 -0
  73. package/src/panel/utils/updateElementDraft.ts +221 -0
  74. package/src/panel/viewportZoom.ts +26 -0
  75. package/src/tailwind.css +43 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @arronqzy/vue-view
2
+
3
+ Vue 3 版 Abuilder **视图编辑器**,使用 **Ant Design Vue** 作为 UI 组件库,功能与 `@arronqzy/react-view` 对齐。
4
+
5
+ ## 功能
6
+
7
+ - **画布**:无限平移/缩放(Infinite Viewer)、Moveable 拖拽/缩放/旋转、Selecto 框选
8
+ - **物料**:图表(ECharts)、文本、图片、音视频、几何、网格、引用节点
9
+ - **图层**:多图层、映射图层、主图层、锁定与合并
10
+ - **配置侧栏**:按物料类型的完整属性编辑、Scope 模版与预览
11
+ - **工作区**:IndexedDB 多项目、导入导出、跨标签页同步
12
+ - **蓝图集成**:分屏蓝图编辑器、调试 Scope 绑定
13
+ - **在线预览**:`VueViewOnlinePreview` 独立预览页
14
+
15
+ ## 使用
16
+
17
+ ```ts
18
+ import { createApp } from "vue";
19
+ import Antd from "ant-design-vue";
20
+ import "ant-design-vue/dist/reset.css";
21
+ import { VueViewPanel } from "@arronqzy/vue-view";
22
+
23
+ createApp(VueViewPanel).use(Antd).mount("#app");
24
+ ```
25
+
26
+ ### 在线预览
27
+
28
+ ```ts
29
+ import { VueViewOnlinePreview, parseOnlinePreviewSearchParams } from "@arronqzy/vue-view";
30
+ ```
31
+
32
+ URL 参数:`?preview=online&projectId=<id>&pid=<instanceId>`
33
+
34
+ ## 依赖
35
+
36
+ - `@arronqzy/rx-store` — 画布状态
37
+ - `@arronqzy/vue-rx-store` — Vue composables
38
+ - `@arronqzy/vue-blueprint` — 蓝图编辑器
39
+ - `@arronqzy/blueprint-dsl` — 蓝图 DSL
40
+ - `ant-design-vue`、`echarts`、`moveable`、`selecto`、`infinite-viewer`
41
+
42
+ ## 与 React 版差异
43
+
44
+ - UI 使用 **Ant Design Vue**,非 shadcn/Radix
45
+ - 标尺使用 canvas 兼容实现(可替换为 `@scena/ruler`)
46
+ - 共享 `@arronqzy/rx-store` 与 `panel/utils` 数据逻辑
47
+
48
+ ## 许可证
49
+
50
+ MIT
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@arronqzy/vue-view",
3
+ "version": "0.1.0",
4
+ "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "src",
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": "./src/index.ts"
13
+ },
14
+ "dependencies": {
15
+ "ant-design-vue": "^4.2.6",
16
+ "dayjs": "^1.11.13",
17
+ "echarts": "^6.0.0",
18
+ "immer": "^11.0.1",
19
+ "infinite-viewer": "^0.29.1",
20
+ "moveable": "^0.53.0",
21
+ "rxjs": "^7.8.2",
22
+ "selecto": "^1.26.3",
23
+ "uuid": "^13.0.0",
24
+ "vue": "^3.5.13",
25
+ "@arronqzy/blueprint-dsl": "1.0.3",
26
+ "@arronqzy/vue-rx-store": "0.1.0",
27
+ "@arronqzy/vue-blueprint": "0.1.0",
28
+ "@arronqzy/rx-store": "1.0.2"
29
+ },
30
+ "peerDependencies": {
31
+ "vue": "^3.4.0"
32
+ },
33
+ "devDependencies": {
34
+ "@vitejs/plugin-vue": "^5.2.1",
35
+ "eslint": "^8.57.0",
36
+ "typescript": "^5.5.4",
37
+ "vue-tsc": "^2.2.0",
38
+ "@arronqzy/eslint-config": "0.0.0",
39
+ "@arronqzy/typescript-config": "1.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "lint": "eslint \"src/**/*.{ts,vue}\"",
46
+ "typecheck": "vue-tsc -p tsconfig.json --noEmit",
47
+ "build": "pnpm run typecheck"
48
+ }
49
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ declare module "echarts" {
2
+ export type EChartsOption = Record<string, unknown>;
3
+ export interface ECharts {
4
+ setOption(option: EChartsOption, notMerge?: boolean): void;
5
+ resize(): void;
6
+ dispose(): void;
7
+ }
8
+ export function init(
9
+ dom: HTMLElement,
10
+ theme?: unknown,
11
+ opts?: { renderer?: "canvas" | "svg" }
12
+ ): ECharts;
13
+ }
14
+
15
+ declare module "infinite-viewer" {
16
+ export type InfiniteViewerOptions = {
17
+ margin?: number;
18
+ threshold?: number;
19
+ useMouseDrag?: boolean;
20
+ useWheelScroll?: boolean;
21
+ preventWheelClick?: boolean;
22
+ displayVerticalScroll?: boolean;
23
+ displayHorizontalScroll?: boolean;
24
+ };
25
+
26
+ export default class InfiniteViewer {
27
+ constructor(
28
+ containerElement: HTMLElement,
29
+ viewportElement?: HTMLElement,
30
+ options?: InfiniteViewerOptions
31
+ );
32
+ on(eventName: "scroll", handler: () => void): void;
33
+ scrollTo(scrollLeft: number, scrollTop: number): void;
34
+ getScrollLeft(): number;
35
+ getScrollTop(): number;
36
+ getContainer(): HTMLElement;
37
+ destroy(): void;
38
+ }
39
+ }
40
+
41
+ declare module "moveable" {
42
+ export type MoveableOptions = Record<string, unknown>;
43
+
44
+ export default class Moveable {
45
+ constructor(container: HTMLElement, options?: MoveableOptions);
46
+ target: HTMLElement | HTMLElement[] | null;
47
+ zoom: number;
48
+ on(eventName: string, handler: (e: any) => void): void;
49
+ updateRect(): void;
50
+ destroy(): void;
51
+ }
52
+ }
53
+
54
+ declare module "selecto" {
55
+ export type SelectoOptions = Record<string, unknown>;
56
+
57
+ export default class Selecto {
58
+ constructor(options?: SelectoOptions);
59
+ on(eventName: string, handler: (e: any) => void): void;
60
+ destroy(): void;
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { default as VueViewPanel } from "./panel/VueViewPanel.vue";
2
+ export { default as VueViewOnlinePreview } from "./panel/VueViewOnlinePreview.vue";
3
+ export { parseOnlinePreviewSearchParams } from "./panel/parseOnlinePreviewSearchParams";
4
+ export type * from "./panel/types";
@@ -0,0 +1,276 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
3
+ import {
4
+ BlueprintGraph,
5
+ documentToRunnableGraph,
6
+ getBlueprintLibraryRecord,
7
+ listBlueprintLibrary,
8
+ useBlueprintPageLifecycle,
9
+ type BlueprintLibraryListItem,
10
+ } from "@arronqzy/vue-blueprint";
11
+ import type { LibraryBlueprintResolver, PageLifecyclePhase } from "@arronqzy/blueprint-dsl";
12
+ import type { State } from "@arronqzy/rx-store";
13
+ import ElementsLayer from "./components/ElementsLayer.vue";
14
+ import {
15
+ clearViewElementScopes,
16
+ getViewElementScope,
17
+ setViewElementScopes,
18
+ useViewScopeStoreVersion,
19
+ } from "./scope/view-scope-store";
20
+ import { resolvePanelElementScope } from "./utils/scope-template";
21
+ import { getWorkspaceProject } from "./library/workspace-project-db";
22
+ import { readWorkspacePreviewCache } from "./library/workspace-project-cache";
23
+ import { subscribeWorkspaceProjectUpdates } from "./library/workspace-project-sync";
24
+ import {
25
+ computePanelSceneBounds,
26
+ getActiveLayerId,
27
+ normalizeImportedPanelState,
28
+ notifyPreviewLayoutChanged,
29
+ parseAllPanelElements,
30
+ parsePanelLayers,
31
+ resolvePreviewLayerElements,
32
+ } from "./utils/panelStateIO";
33
+
34
+ const PREVIEW_BOOT_PHASES: PageLifecyclePhase[] = ["mounted"];
35
+
36
+ const props = defineProps<{
37
+ projectId: string;
38
+ previewInstanceId?: string;
39
+ }>();
40
+
41
+ const loadError = ref<string | null>(null);
42
+ const panelState = ref<State | null>(null);
43
+ const projectRevision = ref(0);
44
+ const blueprintGraph = ref(BlueprintGraph.empty());
45
+ const blueprintLibraryItems = ref<BlueprintLibraryListItem[]>([]);
46
+ const layoutRevision = ref(0);
47
+ const layoutReady = ref(false);
48
+ const sceneRef = ref<HTMLDivElement | null>(null);
49
+
50
+ const layers = computed(() => (panelState.value ? parsePanelLayers(panelState.value) : []));
51
+ const activeLayerId = computed(() =>
52
+ panelState.value ? getActiveLayerId(panelState.value) : "layer-1"
53
+ );
54
+ const allElements = computed(() =>
55
+ panelState.value ? parseAllPanelElements(panelState.value) : []
56
+ );
57
+
58
+ async function loadWorkspaceRecord(projectId: string) {
59
+ const fromDb = await getWorkspaceProject(projectId);
60
+ if (fromDb) return fromDb;
61
+ return readWorkspacePreviewCache(projectId);
62
+ }
63
+
64
+ function applyTitleIcon(titleIconDataUrl?: string) {
65
+ if (!titleIconDataUrl) return;
66
+ for (const rel of ["icon", "shortcut icon"]) {
67
+ document.querySelector(`link[rel='${rel}']`)?.remove();
68
+ const link = document.createElement("link");
69
+ link.rel = rel;
70
+ link.type = "image/png";
71
+ link.href = titleIconDataUrl;
72
+ document.head.appendChild(link);
73
+ }
74
+ }
75
+
76
+ async function loadProject() {
77
+ const record = await loadWorkspaceRecord(props.projectId);
78
+ if (!record) {
79
+ loadError.value = "工作区不存在或已被删除";
80
+ panelState.value = null;
81
+ return;
82
+ }
83
+ const normalized = normalizeImportedPanelState(record.panelState);
84
+ if (!normalized) {
85
+ loadError.value = "工作区数据格式无效";
86
+ panelState.value = null;
87
+ return;
88
+ }
89
+ loadError.value = null;
90
+ clearViewElementScopes();
91
+ panelState.value = normalized;
92
+ blueprintGraph.value = BlueprintGraph.fromDocument(record.blueprintDocument);
93
+ document.title = record.productName.trim() || record.name || "预览";
94
+ applyTitleIcon(record.titleIconDataUrl);
95
+ projectRevision.value += 1;
96
+ }
97
+
98
+ onMounted(() => {
99
+ void loadProject();
100
+ });
101
+
102
+ onMounted(() => {
103
+ const unsub = subscribeWorkspaceProjectUpdates(props.projectId, () => {
104
+ void loadProject();
105
+ });
106
+ onUnmounted(unsub);
107
+ });
108
+
109
+ watch(projectRevision, () => {
110
+ void listBlueprintLibrary().then((items) => {
111
+ blueprintLibraryItems.value = items;
112
+ });
113
+ });
114
+
115
+ const blueprintLibraryNameById = computed(
116
+ () => new Map(blueprintLibraryItems.value.map((item) => [item.id, item.name]))
117
+ );
118
+
119
+ const resolveLibraryBlueprint: LibraryBlueprintResolver = async (libraryBlueprintId) => {
120
+ const record = await getBlueprintLibraryRecord(libraryBlueprintId);
121
+ if (!record) return null;
122
+ const items = await listBlueprintLibrary();
123
+ const nameById = new Map(items.map((item) => [item.id, item.name]));
124
+ return documentToRunnableGraph(record.document, { libraryNameById: nameById });
125
+ };
126
+
127
+ function handleViewScopeUpdate(viewElementIds: string[], scope: unknown) {
128
+ setViewElementScopes(viewElementIds, scope);
129
+ }
130
+
131
+ const layerElements = computed(() =>
132
+ resolvePreviewLayerElements(allElements.value, layers.value, activeLayerId.value)
133
+ );
134
+
135
+ const scopeStoreVersion = useViewScopeStoreVersion();
136
+ const scopedLayerElements = computed(() => {
137
+ void scopeStoreVersion.value;
138
+ return layerElements.value.map((el) =>
139
+ resolvePanelElementScope(el, getViewElementScope(el.id))
140
+ );
141
+ });
142
+
143
+ const sceneBounds = computed(() => computePanelSceneBounds(scopedLayerElements.value));
144
+
145
+ const displayElements = computed(() =>
146
+ scopedLayerElements.value.map((el) => ({
147
+ ...el,
148
+ x: el.x - sceneBounds.value.minX,
149
+ y: el.y - sceneBounds.value.minY,
150
+ }))
151
+ );
152
+
153
+ function applySceneFit() {
154
+ const scene = sceneRef.value;
155
+ if (!scene) return;
156
+ const vw = window.innerWidth || 1;
157
+ const vh = window.innerHeight || 1;
158
+ const sw = Math.max(1, sceneBounds.value.width);
159
+ const sh = Math.max(1, sceneBounds.value.height);
160
+ const scaleX = vw / sw;
161
+ const scaleY = vh / sh;
162
+ if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return;
163
+ scene.style.transform = `scale(${scaleX}, ${scaleY})`;
164
+ layoutRevision.value += 1;
165
+ notifyPreviewLayoutChanged();
166
+ }
167
+
168
+ watch(
169
+ [
170
+ panelState,
171
+ () => displayElements.value.length,
172
+ () => layerElements.value.length,
173
+ sceneBounds,
174
+ projectRevision,
175
+ ],
176
+ () => {
177
+ if (!panelState.value || layerElements.value.length === 0) return;
178
+ layoutReady.value = false;
179
+ applySceneFit();
180
+ requestAnimationFrame(() => {
181
+ applySceneFit();
182
+ requestAnimationFrame(() => {
183
+ notifyPreviewLayoutChanged();
184
+ layoutReady.value = true;
185
+ });
186
+ });
187
+ },
188
+ { deep: true }
189
+ );
190
+
191
+ const lifecycleReady = computed(
192
+ () => Boolean(panelState.value && layerElements.value.length > 0 && layoutReady.value)
193
+ );
194
+
195
+ useBlueprintPageLifecycle({
196
+ graph: blueprintGraph,
197
+ active: ref(true),
198
+ enabled: lifecycleReady,
199
+ bootPhases: PREVIEW_BOOT_PHASES,
200
+ bootKey: computed(() => projectRevision.value),
201
+ waitForPageReady: true,
202
+ onUpdated: computed(
203
+ () => `${activeLayerId.value}|${layerElements.value.length}|${projectRevision.value}|${layoutRevision.value}`
204
+ ),
205
+ resolveLibraryBlueprint,
206
+ libraryNameById: blueprintLibraryNameById,
207
+ rootLibraryBlueprintId: ref(null),
208
+ onViewScopeUpdate: handleViewScopeUpdate,
209
+ });
210
+
211
+ onMounted(() => {
212
+ const onResize = () => applySceneFit();
213
+ window.addEventListener("resize", onResize);
214
+ onUnmounted(() => window.removeEventListener("resize", onResize));
215
+ });
216
+
217
+ function noopUpdate() {}
218
+ function noopSelect() {}
219
+ </script>
220
+
221
+ <template>
222
+ <div
223
+ v-if="loadError"
224
+ class="flex min-h-screen w-full items-center justify-center bg-white px-6 text-center text-sm text-gray-600"
225
+ >
226
+ {{ loadError }}
227
+ </div>
228
+
229
+ <div
230
+ v-else-if="!panelState"
231
+ class="flex min-h-screen w-full items-center justify-center bg-white text-sm text-gray-600"
232
+ >
233
+ 加载预览中…
234
+ </div>
235
+
236
+ <div
237
+ v-else-if="layerElements.length === 0"
238
+ class="flex min-h-screen w-full flex-col items-center justify-center gap-2 bg-white px-6 text-center text-sm text-gray-600"
239
+ >
240
+ <div>当前工作区没有可预览的视图节点</div>
241
+ <div class="text-xs text-gray-400">请先在编辑器中点击「同步」或「创建工作区」后再预览</div>
242
+ </div>
243
+
244
+ <div
245
+ v-else
246
+ class="min-h-screen w-full overflow-hidden bg-white text-gray-900"
247
+ data-preview-mode="online"
248
+ :data-project-id="projectId"
249
+ :data-preview-instance-id="previewInstanceId ?? ''"
250
+ :data-preview-node-count="String(displayElements.length)"
251
+ >
252
+ <div id="preview-root" class="overflow-hidden" style="width: 100vw; height: 100vh">
253
+ <div
254
+ ref="sceneRef"
255
+ id="preview-scene"
256
+ class="relative shrink-0 origin-top-left"
257
+ :style="{
258
+ width: `${sceneBounds.width}px`,
259
+ height: `${sceneBounds.height}px`,
260
+ transformOrigin: 'left top',
261
+ }"
262
+ >
263
+ <ElementsLayer
264
+ :elements="displayElements"
265
+ :all-elements="allElements"
266
+ :selected-ids="[]"
267
+ :update-element="noopUpdate"
268
+ :layer-locked="true"
269
+ :preview-mode="true"
270
+ :preview-layout-key="layoutRevision"
271
+ @select-ids="noopSelect"
272
+ />
273
+ </div>
274
+ </div>
275
+ </div>
276
+ </template>