@arronqzy/vue-view 0.1.20 → 0.1.21

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 CHANGED
@@ -1,16 +1,22 @@
1
1
  # @arronqzy/vue-view
2
2
 
3
- Vue 3 版 Abuilder **视图编辑器**,使用 **Ant Design Vue** 作为 UI 组件库,功能与 `@arronqzy/react-view` 对齐。
3
+ Vue 3 视图编辑器,功能对齐 `@arronqzy/react-view`,组件用 Ant Design Vue。大多数 Vue 宿主应直接用 [`@arronqzy/abuilder-vue`](../abuilder-vue/README.md)。
4
4
 
5
- ## 功能
5
+ ![物料、画布、配置和图层](https://github.com/qianzhuoyao/APanel5/raw/v5/docs/guide-assets/live-zh/09-table-full.png)
6
6
 
7
- - **画布**:无限平移/缩放(Infinite Viewer)、Moveable 拖拽/缩放/旋转、Selecto 框选
8
- - **物料**:图表(ECharts)、文本、图片、音视频、几何、网格、引用节点
9
- - **图层**:多图层、映射图层、主图层、锁定与合并
10
- - **配置侧栏**:按物料类型的完整属性编辑、Scope 模版与预览
11
- - **工作区**:IndexedDB 多项目、导入导出、跨标签页同步
12
- - **蓝图集成**:分屏蓝图编辑器、调试 Scope 绑定
13
- - **在线预览**:`VueViewOnlinePreview` 独立预览页
7
+ ## 能力
8
+
9
+ - 无限平移 / 缩放(Infinite Viewer)、Moveable、Selecto 框选
10
+ - 图表、文本、表格、图片、音视频、几何、网格、三维、引用节点
11
+ - 多图层、映射图层、主图层、锁定与合并
12
+ - 按物料类型的配置侧栏,支持 Scope 模版
13
+ - IndexedDB 多项目、导入导出、跨标签同步
14
+ - 分屏蓝图(`@arronqzy/vue-blueprint`)和调试时的 Scope 回写
15
+ - `VueViewOnlinePreview` 只读预览
16
+
17
+ 打开蓝图后的分屏和 React 版一样:上面视图,下面节点:
18
+
19
+ ![分屏蓝图](https://github.com/qianzhuoyao/APanel5/raw/v5/docs/guide-assets/live-zh/40-blueprint-open.png)
14
20
 
15
21
  ## 使用
16
22
 
@@ -29,7 +35,7 @@ createApp(VueViewPanel).use(Antd).mount("#app");
29
35
  import { VueViewOnlinePreview, parseOnlinePreviewSearchParams } from "@arronqzy/vue-view";
30
36
  ```
31
37
 
32
- URL 参数:`?preview=online&projectId=<id>&pid=<instanceId>`。也可传入 `workspace` 直接预览宿主数据(优先于 `projectId`)。
38
+ URL:`?preview=online&projectId=<id>&pid=<instanceId>`。传入 `workspace` 时优先于 `projectId`。
33
39
 
34
40
  ### 宿主集成
35
41
 
@@ -43,21 +49,19 @@ import {
43
49
  } from "@arronqzy/vue-view";
44
50
  ```
45
51
 
46
- 创建 / 同步工作区会发出 `workspace:add` / `workspace:sync`。`parseWorkspaceData` 的 `value` 可作 `initialWorkspace`。IndexedDB CRUD 与 `getPreviewSnapshot` **不导出**(后者仅 React 实现)。
52
+ 创建 / 同步发出 `workspace:add` / `workspace:sync`。`parseWorkspaceData` 的 `value` 可作 `initialWorkspace`。
47
53
 
48
- ## 依赖
54
+ IndexedDB CRUD 与 `getPreviewSnapshot` **不导出**。缩略图只在 React 包实现。
55
+
56
+ ## 和 React 版的差别
49
57
 
50
- - `@arronqzy/rx-store` — 画布状态
51
- - `@arronqzy/vue-rx-store` — Vue composables
52
- - `@arronqzy/vue-blueprint` — 蓝图编辑器
53
- - `@arronqzy/blueprint-dsl` — 蓝图 DSL
54
- - `ant-design-vue`、`echarts`、`moveable`、`selecto`、`infinite-viewer`
58
+ - UI 是 Ant Design Vue
59
+ - 标尺是 canvas 实现(可换成 `@scena/ruler`)
60
+ - 画布状态仍是共享的 `@arronqzy/rx-store`
55
61
 
56
- ## 与 React 版差异
62
+ ## 依赖
57
63
 
58
- - UI 使用 **Ant Design Vue**,非 shadcn/Radix
59
- - 标尺使用 canvas 兼容实现(可替换为 `@scena/ruler`)
60
- - 共享 `@arronqzy/rx-store` 与 `panel/utils` 数据逻辑
64
+ `@arronqzy/rx-store`、`@arronqzy/vue-rx-store`、`@arronqzy/vue-blueprint`、`@arronqzy/blueprint-dsl`、`ant-design-vue`、`echarts`、`moveable`、`selecto`、`infinite-viewer`。
61
65
 
62
66
  ## 许可证
63
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,12 +23,12 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/blueprint-dsl": "1.0.15",
27
- "@arronqzy/i18n": "0.1.14",
28
- "@arronqzy/view-table": "0.1.8",
26
+ "@arronqzy/blueprint-dsl": "1.0.16",
29
27
  "@arronqzy/view-scene3d": "0.1.5",
30
- "@arronqzy/vue-blueprint": "0.1.14",
28
+ "@arronqzy/view-table": "0.1.8",
29
+ "@arronqzy/i18n": "0.1.16",
31
30
  "@arronqzy/vue-rx-store": "0.1.6",
31
+ "@arronqzy/vue-blueprint": "0.1.16",
32
32
  "@arronqzy/rx-store": "1.0.8"
33
33
  },
34
34
  "peerDependencies": {
@@ -39,8 +39,8 @@
39
39
  "eslint": "^8.57.0",
40
40
  "typescript": "^5.5.4",
41
41
  "vue-tsc": "^2.2.0",
42
- "@arronqzy/eslint-config": "0.0.0",
43
- "@arronqzy/typescript-config": "1.0.0"
42
+ "@arronqzy/typescript-config": "1.0.0",
43
+ "@arronqzy/eslint-config": "0.0.0"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
package/src/index.ts CHANGED
@@ -22,6 +22,8 @@ export type {
22
22
  AbuilderEventPayloadMap,
23
23
  WorkspaceAddEventPayload,
24
24
  WorkspaceSyncEventPayload,
25
+ ApiCollectionUploadStartEventPayload,
26
+ ApiCollectionUploadSuccessEventPayload,
25
27
  WorkspaceData,
26
28
  } from "./panel/library/event-subscription";
27
29
  export type {
@@ -2,6 +2,7 @@
2
2
  import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
3
3
  import type { State } from "@arronqzy/rx-store";
4
4
  import {
5
+ ApiCollectionManagerDialog,
5
6
  BlueprintGraph,
6
7
  abortClockNode,
7
8
  blueprintDocumentsEqual,
@@ -12,6 +13,7 @@ import {
12
13
  documentToRunnableGraph,
13
14
  downloadBlueprintExport,
14
15
  getBlueprintLibraryRecord,
16
+ hydrateApiCollections,
15
17
  libraryRecordFromImport,
16
18
  listBlueprintLibrary,
17
19
  parseBlueprintImportFile,
@@ -27,7 +29,7 @@ import {
27
29
  resolveRunnableNodeType,
28
30
  resolveViewElementIds,
29
31
  } from "@arronqzy/vue-blueprint";
30
- import { collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE, type LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
32
+ import { collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE, buildApiCollectionSyncPatches, toApiCollectionSyncResult, type LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
31
33
  import {
32
34
  Button,
33
35
  Checkbox,
@@ -49,6 +51,7 @@ import {
49
51
  useWorkspaceProjects,
50
52
  } from "./hooks/useWorkspaceProjects";
51
53
  import type { WorkspaceProjectRecord } from "./library/workspace-project-db";
54
+ import { AbuilderEvents, emitAbuilderEvent } from "./library/event-subscription";
52
55
  import { type ViewportZoom } from "./viewportZoom";
53
56
  import PanelCanvas from "./components/PanelCanvas.vue";
54
57
  import ElementsLayer from "./components/ElementsLayer.vue";
@@ -158,6 +161,7 @@ const leftWidth = ref(240);
158
161
  const rightWidth = ref(300);
159
162
  const importInputRef = ref<HTMLInputElement | null>(null);
160
163
  const blueprintImportInputRef = ref<HTMLInputElement | null>(null);
164
+ const apiCollectionManagerOpen = ref(false);
161
165
 
162
166
  const viewportEl = ref<HTMLDivElement | null>(null);
163
167
  const canvasEl = ref<HTMLDivElement | null>(null);
@@ -391,6 +395,16 @@ async function refreshBlueprintLibrary() {
391
395
  blueprintLibraryItems.value = await listBlueprintLibrary(props.nameSpace);
392
396
  }
393
397
 
398
+ watch(
399
+ () => props.nameSpace,
400
+ (nameSpace) => {
401
+ void hydrateApiCollections(nameSpace).catch((error) => {
402
+ console.error("[abuilder] hydrate api collections failed", error);
403
+ });
404
+ },
405
+ { immediate: true }
406
+ );
407
+
394
408
  onMounted(() => {
395
409
  void refreshBlueprintLibrary();
396
410
  });
@@ -604,6 +618,50 @@ function openBlueprintExport() {
604
618
  });
605
619
  }
606
620
 
621
+ function handleApiCollectionUploadStart(info: {
622
+ fileNames: string[];
623
+ count: number;
624
+ }) {
625
+ emitAbuilderEvent(AbuilderEvents.apiCollectionUploadStart, info);
626
+ }
627
+
628
+ function handleApiCollectionUploadSuccess(info: {
629
+ collections: Array<{
630
+ id: string;
631
+ name: string;
632
+ apiCount: number;
633
+ persistLocal: boolean;
634
+ }>;
635
+ }) {
636
+ emitAbuilderEvent(AbuilderEvents.apiCollectionUploadSuccess, info);
637
+ }
638
+
639
+ function handleApiCollectionSync(record: {
640
+ id: string;
641
+ document: import("@arronqzy/blueprint-dsl").ApiCollectionDocument;
642
+ }) {
643
+ const patches = buildApiCollectionSyncPatches({
644
+ collectionId: record.id,
645
+ document: record.document,
646
+ nodes: blueprintGraph.value.document.nodes
647
+ .filter((node) => node.role === "fetch" || Boolean(node.fetchConfig))
648
+ .map((node) => ({
649
+ id: node.id,
650
+ label: node.label,
651
+ fetchConfig: node.fetchConfig,
652
+ })),
653
+ });
654
+ if (patches.length === 0) return toApiCollectionSyncResult([]);
655
+ let next = blueprintGraph.value;
656
+ for (const patch of patches) {
657
+ next = next.updateNode(patch.nodeId, {
658
+ fetchConfig: patch.fetchConfigPatch as import("@arronqzy/blueprint-dsl").FetchRequestConfig,
659
+ });
660
+ }
661
+ blueprintGraph.value = next;
662
+ return toApiCollectionSyncResult(patches);
663
+ }
664
+
607
665
  async function handleBlueprintImportFile(file: File) {
608
666
  try {
609
667
  await runBusyTask(t("common.importingBlueprint"), async () => {
@@ -757,6 +815,8 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
757
815
  <Menu>
758
816
  <Menu.Item @click="openBlueprintExport">{{ t("panel.menubar.exportBlueprint") }}</Menu.Item>
759
817
  <Menu.Item @click="blueprintImportInputRef?.click()">{{ t("panel.menubar.importBlueprint") }}</Menu.Item>
818
+ <Menu.Divider />
819
+ <Menu.Item @click="apiCollectionManagerOpen = true">{{ t("panel.menubar.apiCollections") }}</Menu.Item>
760
820
  </Menu>
761
821
  </template>
762
822
  </Dropdown>
@@ -1021,6 +1081,14 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
1021
1081
  <Input v-model:value="mergeLayerName" :placeholder="t('panel.layers.mergedNamePlaceholderOptional')" />
1022
1082
  <div class="mt-2 text-xs text-gray-500">{{ t("panel.layers.mergeSelectedCount", { count: mergeSelectedCount }) }}</div>
1023
1083
  </Modal>
1084
+
1085
+ <ApiCollectionManagerDialog
1086
+ :open="apiCollectionManagerOpen"
1087
+ :on-open-change="(open) => (apiCollectionManagerOpen = open)"
1088
+ :on-upload-start="handleApiCollectionUploadStart"
1089
+ :on-upload-success="handleApiCollectionUploadSuccess"
1090
+ :on-sync-collection="handleApiCollectionSync"
1091
+ />
1024
1092
  </Layout>
1025
1093
  </template>
1026
1094
 
@@ -69,17 +69,13 @@ function closeDropdown() {
69
69
  }
70
70
 
71
71
  function refreshForInput(input: HTMLInputElement | HTMLTextAreaElement) {
72
- if (props.scope === undefined) {
73
- closeDropdown();
74
- return;
75
- }
76
72
  const cursor = input.selectionStart ?? input.value.length;
77
- const parsed = parseScopeAutocomplete(input.value, cursor, props.scope);
73
+ const parsed = parseScopeAutocomplete(input.value, cursor, props.scope ?? {});
78
74
  if (!parsed) {
79
75
  closeDropdown();
80
76
  return;
81
77
  }
82
- const suggestions = getScopeAutocompleteSuggestions(props.scope, parsed);
78
+ const suggestions = getScopeAutocompleteSuggestions(props.scope ?? {}, parsed);
83
79
  if (suggestions.length === 0) {
84
80
  closeDropdown();
85
81
  return;
@@ -173,7 +169,7 @@ function onPointerDown(event: MouseEvent) {
173
169
  watch(
174
170
  () => toValue(props.containerRef),
175
171
  (container, _prev, onCleanup) => {
176
- if (!container || props.scope === undefined) return;
172
+ if (!container) return;
177
173
 
178
174
  container.addEventListener("focusin", onFocusIn);
179
175
  container.addEventListener("input", onInput, true);
@@ -197,13 +193,14 @@ watch(
197
193
  watch(
198
194
  () => props.scope,
199
195
  () => {
200
- if (props.scope === undefined) closeDropdown();
196
+ const current = dropdown.value;
197
+ if (current) refreshForInput(current.input);
201
198
  }
202
199
  );
203
200
  </script>
204
201
 
205
202
  <template>
206
- <Teleport v-if="dropdown && scope !== undefined" to="body">
203
+ <Teleport v-if="dropdown" to="body">
207
204
  <div
208
205
  data-scope-ac-list
209
206
  class="fixed z-[10120] max-h-44 min-w-[140px] overflow-auto rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-md"
@@ -7,5 +7,6 @@ const { t } = useI18n();
7
7
  <div class="space-y-2">
8
8
  <p>{{ t("panel.scope.usageHint1") }}</p>
9
9
  <p>{{ t("panel.scope.usageHint2") }}</p>
10
+ <p>{{ t("panel.scope.usageHintSystem") }}</p>
10
11
  </div>
11
12
  </template>
@@ -5,6 +5,10 @@ export const AbuilderEvents = {
5
5
  workspaceAdd: "workspace:add",
6
6
  /** 同步工作区 */
7
7
  workspaceSync: "workspace:sync",
8
+ /** 开始上传接口集合 */
9
+ apiCollectionUploadStart: "api-collection:upload-start",
10
+ /** 上传接口集合成功 */
11
+ apiCollectionUploadSuccess: "api-collection:upload-success",
8
12
  } as const;
9
13
 
10
14
  export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
@@ -15,9 +19,25 @@ export type WorkspaceData = WorkspaceProjectRecord;
15
19
  export type WorkspaceAddEventPayload = WorkspaceData;
16
20
  export type WorkspaceSyncEventPayload = WorkspaceData;
17
21
 
22
+ export type ApiCollectionUploadStartEventPayload = {
23
+ fileNames: string[];
24
+ count: number;
25
+ };
26
+
27
+ export type ApiCollectionUploadSuccessEventPayload = {
28
+ collections: Array<{
29
+ id: string;
30
+ name: string;
31
+ apiCount: number;
32
+ persistLocal: boolean;
33
+ }>;
34
+ };
35
+
18
36
  export type AbuilderEventPayloadMap = {
19
37
  [AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
20
38
  [AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
39
+ [AbuilderEvents.apiCollectionUploadStart]: ApiCollectionUploadStartEventPayload;
40
+ [AbuilderEvents.apiCollectionUploadSuccess]: ApiCollectionUploadSuccessEventPayload;
21
41
  };
22
42
 
23
43
  type EventCallback = (payload: unknown) => void;
@@ -1,6 +1,10 @@
1
1
  import type { PanelElement } from "../types";
2
2
  import type { PanelTableConfig } from "@arronqzy/view-table";
3
3
  import { isUsableTableRawData } from "@arronqzy/view-table";
4
+ import {
5
+ createSystemRoot,
6
+ isDayjsValue,
7
+ } from "@arronqzy/blueprint-dsl";
4
8
  import { materializeChartLabelsFromScope, materializeChartValuesFromScope } from "./scope-template-chart";
5
9
  import {
6
10
  SCOPE_SPREAD_TEMPLATE_RE,
@@ -11,12 +15,12 @@ import {
11
15
  resolveSpreadScopePath,
12
16
  } from "./scope-template-spread";
13
17
 
14
- const SCOPE_TOKEN_RE = /\{(scope[^}]*)\}/g;
18
+ const TEMPLATE_TOKEN_RE = /\{((?:scope|system)[^}]*)\}/g;
15
19
 
16
20
  export function hasScopeTemplate(value: string): boolean {
17
- SCOPE_TOKEN_RE.lastIndex = 0;
21
+ TEMPLATE_TOKEN_RE.lastIndex = 0;
18
22
  SCOPE_SPREAD_TEMPLATE_RE.lastIndex = 0;
19
- return SCOPE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
23
+ return TEMPLATE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
20
24
  }
21
25
 
22
26
  export function evaluateScopeExpression(
@@ -26,8 +30,13 @@ export function evaluateScopeExpression(
26
30
  const trimmed = expression.trim();
27
31
  if (!trimmed) return undefined;
28
32
  try {
29
- const fn = new Function("scope", `"use strict"; return (${trimmed});`);
30
- return fn(scope);
33
+ const system = createSystemRoot();
34
+ const fn = new Function(
35
+ "scope",
36
+ "system",
37
+ `"use strict"; return (${trimmed});`
38
+ );
39
+ return fn(scope, system);
31
40
  } catch {
32
41
  return undefined;
33
42
  }
@@ -39,6 +48,9 @@ function stringifyScopeValue(value: unknown): string {
39
48
  if (typeof value === "number" || typeof value === "boolean") {
40
49
  return String(value);
41
50
  }
51
+ if (isDayjsValue(value)) {
52
+ return value.toISOString();
53
+ }
42
54
  try {
43
55
  return JSON.stringify(value);
44
56
  } catch {
@@ -60,8 +72,8 @@ export function evaluateScopeTemplate(
60
72
  }
61
73
  );
62
74
 
63
- // Only `{scope...}` tokens — keep ECharts / CSS braces like `{b}` intact.
64
- return withSpread.replace(SCOPE_TOKEN_RE, (_: string, rawExpr: string) =>
75
+ // Only `{scope...}` / `{system...}` — keep ECharts / CSS braces like `{b}` intact.
76
+ return withSpread.replace(TEMPLATE_TOKEN_RE, (_: string, rawExpr: string) =>
65
77
  stringifyScopeValue(evaluateScopeExpression(rawExpr, scope))
66
78
  );
67
79
  }
@@ -91,7 +103,7 @@ function materializeTemplateValue(template: string, scope: unknown): unknown {
91
103
  );
92
104
  }
93
105
 
94
- const singleMatch = /^\{(scope[^}]*)\}$/.exec(trimmed);
106
+ const singleMatch = /^\{((?:scope|system)[^}]*)\}$/.exec(trimmed);
95
107
  if (singleMatch) {
96
108
  return evaluateScopeExpression(singleMatch[1]!, scope);
97
109
  }