@arronqzy/vue-view 0.1.19 → 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.19",
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,13 +23,13 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/i18n": "0.1.13",
27
- "@arronqzy/blueprint-dsl": "1.0.15",
26
+ "@arronqzy/blueprint-dsl": "1.0.16",
28
27
  "@arronqzy/view-scene3d": "0.1.5",
29
28
  "@arronqzy/view-table": "0.1.8",
30
- "@arronqzy/rx-store": "1.0.8",
31
- "@arronqzy/vue-blueprint": "0.1.14",
32
- "@arronqzy/vue-rx-store": "0.1.6"
29
+ "@arronqzy/i18n": "0.1.16",
30
+ "@arronqzy/vue-rx-store": "0.1.6",
31
+ "@arronqzy/vue-blueprint": "0.1.16",
32
+ "@arronqzy/rx-store": "1.0.8"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "vue": "^3.4.0"
@@ -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";
@@ -83,10 +86,26 @@ const props = withDefaults(
83
86
  initialZoom?: number;
84
87
  nameSpace?: string | null;
85
88
  initialWorkspace?: WorkspaceProjectRecord | null;
89
+ /** 顶栏「设置 → 关于」展示的 npm 包名 */
90
+ packageName?: string | null;
91
+ /** 顶栏「设置 → 关于」展示的包版本号 */
92
+ packageVersion?: string | null;
86
93
  }>(),
87
- { initialZoom: 1, nameSpace: null, initialWorkspace: null }
94
+ {
95
+ initialZoom: 1,
96
+ nameSpace: null,
97
+ initialWorkspace: null,
98
+ packageName: null,
99
+ packageVersion: null,
100
+ }
88
101
  );
89
102
 
103
+ const packageVersionText = computed(() => {
104
+ const version = (props.packageVersion ?? "").trim();
105
+ if (!version) return t("panel.menubar.versionUnavailable");
106
+ return t("panel.menubar.versionValue", { version });
107
+ });
108
+
90
109
  const {
91
110
  elements,
92
111
  allElements,
@@ -142,6 +161,7 @@ const leftWidth = ref(240);
142
161
  const rightWidth = ref(300);
143
162
  const importInputRef = ref<HTMLInputElement | null>(null);
144
163
  const blueprintImportInputRef = ref<HTMLInputElement | null>(null);
164
+ const apiCollectionManagerOpen = ref(false);
145
165
 
146
166
  const viewportEl = ref<HTMLDivElement | null>(null);
147
167
  const canvasEl = ref<HTMLDivElement | null>(null);
@@ -375,6 +395,16 @@ async function refreshBlueprintLibrary() {
375
395
  blueprintLibraryItems.value = await listBlueprintLibrary(props.nameSpace);
376
396
  }
377
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
+
378
408
  onMounted(() => {
379
409
  void refreshBlueprintLibrary();
380
410
  });
@@ -588,6 +618,50 @@ function openBlueprintExport() {
588
618
  });
589
619
  }
590
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
+
591
665
  async function handleBlueprintImportFile(file: File) {
592
666
  try {
593
667
  await runBusyTask(t("common.importingBlueprint"), async () => {
@@ -741,6 +815,8 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
741
815
  <Menu>
742
816
  <Menu.Item @click="openBlueprintExport">{{ t("panel.menubar.exportBlueprint") }}</Menu.Item>
743
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>
744
820
  </Menu>
745
821
  </template>
746
822
  </Dropdown>
@@ -756,7 +832,29 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
756
832
  @delete-project="handleWorkspaceDeleteProject"
757
833
  @preview-project="openOnlinePreviewForProject"
758
834
  />
759
- <Dropdown :trigger="['click']">
835
+ <Dropdown :trigger="['click']">
836
+ <Button type="text" class="!text-white">{{ t("panel.menubar.settings") }}</Button>
837
+ <template #overlay>
838
+ <Menu>
839
+ <Menu.ItemGroup :title="t('panel.theme.language')">
840
+ <Menu.Item :class="{ 'ant-menu-item-selected': locale === 'zh-CN' }" @click="setLocale('zh-CN')">{{ t("panel.theme.zhCN") }}</Menu.Item>
841
+ <Menu.Item :class="{ 'ant-menu-item-selected': locale === 'en-US' }" @click="setLocale('en-US')">{{ t("panel.theme.enUS") }}</Menu.Item>
842
+ </Menu.ItemGroup>
843
+ <Menu.Divider />
844
+ <Menu.SubMenu :title="t('panel.menubar.about')" key="about">
845
+ <Menu.Item
846
+ key="version"
847
+ disabled
848
+ class="!cursor-default !opacity-100"
849
+ :data-abuilder-version="props.packageVersion || undefined"
850
+ >
851
+ {{ packageVersionText }}
852
+ </Menu.Item>
853
+ </Menu.SubMenu>
854
+ </Menu>
855
+ </template>
856
+ </Dropdown>
857
+ <Dropdown :trigger="['click']">
760
858
  <Button type="text" class="!text-white">{{ t("panel.theme.language") }}</Button>
761
859
  <template #overlay>
762
860
  <Menu>
@@ -983,6 +1081,14 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
983
1081
  <Input v-model:value="mergeLayerName" :placeholder="t('panel.layers.mergedNamePlaceholderOptional')" />
984
1082
  <div class="mt-2 text-xs text-gray-500">{{ t("panel.layers.mergeSelectedCount", { count: mergeSelectedCount }) }}</div>
985
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
+ />
986
1092
  </Layout>
987
1093
  </template>
988
1094
 
@@ -62,8 +62,10 @@ const isSearchCollapsed = ref(false);
62
62
  const sidebarScrollRef = ref<HTMLElement | null>(null);
63
63
  const configScrollPosById = new Map<string, number>();
64
64
  const activeConfigScrollKey = ref<string | null>(null);
65
+ const expandedSectionsById = new Map<string, Record<string, boolean>>();
66
+ const activeExpandedKey = ref<string | null>(null);
65
67
 
66
- const expandedSections = ref<Record<string, boolean>>({
68
+ const DEFAULT_EXPANDED_SECTIONS: Record<string, boolean> = {
67
69
  nodeInfo: true,
68
70
  styleBackground: false,
69
71
  styleBorder: false,
@@ -78,6 +80,10 @@ const expandedSections = ref<Record<string, boolean>>({
78
80
  reference: true,
79
81
  imageConfig: true,
80
82
  tableConfig: true,
83
+ };
84
+
85
+ const expandedSections = ref<Record<string, boolean>>({
86
+ ...DEFAULT_EXPANDED_SECTIONS,
81
87
  });
82
88
 
83
89
  const normalizedSearch = computed(() => configSearch.value.trim().toLowerCase());
@@ -118,12 +124,39 @@ watch(
118
124
  }
119
125
  activeConfigScrollKey.value = nextKey;
120
126
  void nextTick(() => restoreConfigScroll(nextKey));
127
+
128
+ if (prevKey && prevKey !== nextKey) {
129
+ expandedSectionsById.set(prevKey, { ...expandedSections.value });
130
+ }
131
+ activeExpandedKey.value = nextKey;
132
+ if (!nextKey) {
133
+ expandedSections.value = { ...DEFAULT_EXPANDED_SECTIONS };
134
+ return;
135
+ }
136
+ const saved = expandedSectionsById.get(nextKey);
137
+ if (saved) {
138
+ expandedSections.value = { ...saved };
139
+ return;
140
+ }
141
+ const initial = { ...DEFAULT_EXPANDED_SECTIONS };
142
+ expandedSectionsById.set(nextKey, initial);
143
+ expandedSections.value = initial;
121
144
  },
122
145
  { flush: "post" }
123
146
  );
124
147
 
125
148
  onMounted(() => {
126
149
  activeConfigScrollKey.value = configScrollKey.value;
150
+ activeExpandedKey.value = configScrollKey.value;
151
+ if (configScrollKey.value) {
152
+ const saved = expandedSectionsById.get(configScrollKey.value);
153
+ if (saved) expandedSections.value = { ...saved };
154
+ else {
155
+ const initial = { ...DEFAULT_EXPANDED_SECTIONS };
156
+ expandedSectionsById.set(configScrollKey.value, initial);
157
+ expandedSections.value = initial;
158
+ }
159
+ }
127
160
  const el = sidebarScrollRef.value;
128
161
  el?.addEventListener("scroll", onConfigSidebarScroll, { passive: true });
129
162
  restoreConfigScroll(configScrollKey.value);
@@ -171,13 +204,23 @@ const materialType = computed(() => props.selectedElement?.materialType ?? "");
171
204
 
172
205
  const forceOpenSections = computed(() => hasSearch.value);
173
206
  provide("configHasSearch", hasSearch);
207
+ provide("configExpandState", {
208
+ isExpanded: (key: string, defaultValue = true) =>
209
+ expandedSections.value[key] ?? defaultValue,
210
+ setExpanded: (key: string, next: boolean) => {
211
+ setSectionExpanded(key, next);
212
+ },
213
+ });
174
214
 
175
215
  function isSectionExpanded(key: string, defaultValue = true) {
176
216
  return expandedSections.value[key] ?? defaultValue;
177
217
  }
178
218
 
179
219
  function setSectionExpanded(key: string, next: boolean) {
180
- expandedSections.value = { ...expandedSections.value, [key]: next };
220
+ const updated = { ...expandedSections.value, [key]: next };
221
+ expandedSections.value = updated;
222
+ const id = activeExpandedKey.value;
223
+ if (id) expandedSectionsById.set(id, updated);
181
224
  }
182
225
 
183
226
  function shouldShowSection(key: string, title: string, searchTerms: string[] = []) {
@@ -223,15 +266,6 @@ onMounted(() => {
223
266
  watch(isSearchCollapsed, (next) => {
224
267
  window.localStorage.setItem(SEARCH_COLLAPSE_STORAGE_KEY, next ? "1" : "0");
225
268
  });
226
-
227
- watch(
228
- () => [props.selectedElement?.id, props.selectedElement?.materialType],
229
- () => {
230
- if (isChartElement.value) {
231
- setSectionExpanded("chartBasic", true);
232
- }
233
- }
234
- );
235
269
  </script>
236
270
 
237
271
  <template>
@@ -2,12 +2,19 @@
2
2
  import { computed, inject, ref, type ComputedRef } from "vue";
3
3
  import ConfigHintIcon from "../ConfigHintIcon.vue";
4
4
 
5
+ export type ConfigExpandState = {
6
+ isExpanded: (key: string, defaultValue?: boolean) => boolean;
7
+ setExpanded: (key: string, next: boolean) => void;
8
+ };
9
+
5
10
  const props = withDefaults(
6
11
  defineProps<{
7
12
  title: string;
8
13
  hint?: boolean;
9
14
  collapsible?: boolean;
10
15
  defaultOpen?: boolean;
16
+ /** Stable key for remembering expand state per view node */
17
+ groupKey?: string;
11
18
  }>(),
12
19
  { defaultOpen: true, collapsible: false }
13
20
  );
@@ -16,13 +23,30 @@ const hasSearch = inject<ComputedRef<boolean>>(
16
23
  "configHasSearch",
17
24
  computed(() => false)
18
25
  );
26
+ const expandState = inject<ConfigExpandState | null>("configExpandState", null);
27
+
28
+ const localOpen = ref(props.defaultOpen);
29
+ const storageKey = computed(
30
+ () => props.groupKey?.trim() || `fg:${props.title}`
31
+ );
19
32
 
20
- const open = ref(props.defaultOpen);
21
- const isOpen = computed(() => !props.collapsible || hasSearch.value || open.value);
33
+ const isOpen = computed(() => {
34
+ if (!props.collapsible) return true;
35
+ if (hasSearch.value) return true;
36
+ if (expandState) {
37
+ return expandState.isExpanded(storageKey.value, props.defaultOpen);
38
+ }
39
+ return localOpen.value;
40
+ });
22
41
 
23
42
  function toggle() {
24
43
  if (!props.collapsible || hasSearch.value) return;
25
- open.value = !open.value;
44
+ const next = !isOpen.value;
45
+ if (expandState) {
46
+ expandState.setExpanded(storageKey.value, next);
47
+ return;
48
+ }
49
+ localOpen.value = next;
26
50
  }
27
51
  </script>
28
52
 
@@ -186,6 +186,7 @@ function onPosterFileChange(e: Event) {
186
186
  </ConfigFieldGroup>
187
187
  <ConfigFieldGroup
188
188
  :title="t('panel.config.groupDisplayStyle')"
189
+ group-key="audioDisplayStyle"
189
190
  collapsible
190
191
  :default-open="false"
191
192
  >
@@ -238,6 +238,7 @@ function onOptionJsonChange(v: string) {
238
238
  </ConfigFieldGroup>
239
239
  <ConfigFieldGroup
240
240
  :title="t('panel.config.groupChartDisplayMore')"
241
+ group-key="chartDisplayMore"
241
242
  collapsible
242
243
  :default-open="false"
243
244
  >
@@ -308,6 +309,7 @@ function onOptionJsonChange(v: string) {
308
309
 
309
310
  <ConfigFieldGroup
310
311
  :title="t('panel.config.groupTooltip')"
312
+ group-key="chartTooltip"
311
313
  collapsible
312
314
  :default-open="false"
313
315
  >
@@ -369,6 +371,7 @@ function onOptionJsonChange(v: string) {
369
371
  <ConfigFieldGroup
370
372
  v-if="['bar', 'line', 'area', 'scatter'].includes(selectedChartType)"
371
373
  :title="t('panel.config.groupAxes')"
374
+ group-key="chartAxes"
372
375
  collapsible
373
376
  :default-open="false"
374
377
  >
@@ -140,6 +140,7 @@ function clearSketch() {
140
140
  </ConfigFieldGroup>
141
141
  <ConfigFieldGroup
142
142
  :title="t('panel.config.groupCanvasScript')"
143
+ group-key="geometryCanvasScript"
143
144
  collapsible
144
145
  :default-open="false"
145
146
  >
@@ -158,6 +159,7 @@ function clearSketch() {
158
159
  </ConfigFieldGroup>
159
160
  <ConfigFieldGroup
160
161
  :title="t('panel.config.groupSketchOverlay')"
162
+ group-key="geometrySketch"
161
163
  collapsible
162
164
  :default-open="false"
163
165
  >
@@ -100,6 +100,7 @@ function onFileChange(e: Event) {
100
100
  </ConfigFieldGroup>
101
101
  <ConfigFieldGroup
102
102
  :title="t('panel.config.groupDisplayMode')"
103
+ group-key="imageDisplayMode"
103
104
  collapsible
104
105
  :default-open="false"
105
106
  >
@@ -101,6 +101,7 @@ function patch(patch: Partial<PanelElement>) {
101
101
  </div>
102
102
  <ConfigFieldGroup
103
103
  :title="t('panel.config.groupNodeMore')"
104
+ group-key="nodeMore"
104
105
  collapsible
105
106
  :default-open="false"
106
107
  >
@@ -59,6 +59,7 @@ function patch(patch: Partial<PanelElement>) {
59
59
  </ConfigFieldGroup>
60
60
  <ConfigFieldGroup
61
61
  :title="t('panel.config.groupCopyStrategy')"
62
+ group-key="referenceCopyStrategy"
62
63
  collapsible
63
64
  :default-open="false"
64
65
  >
@@ -84,6 +84,7 @@ function onFileChange(e: Event) {
84
84
  </ConfigFieldGroup>
85
85
  <ConfigFieldGroup
86
86
  :title="t('panel.config.groupBgLayout')"
87
+ group-key="styleBgLayout"
87
88
  collapsible
88
89
  :default-open="false"
89
90
  >
@@ -215,6 +215,7 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
215
215
  </ConfigFieldGroup>
216
216
  <ConfigFieldGroup
217
217
  :title="t('panel.config.groupInputAbility')"
218
+ group-key="textInputAbility"
218
219
  collapsible
219
220
  :default-open="false"
220
221
  >
@@ -74,6 +74,7 @@ function patch(next: Partial<PanelElement>) {
74
74
  </ConfigFieldGroup>
75
75
  <ConfigFieldGroup
76
76
  :title="t('panel.config.groupCopyStrategy')"
77
+ group-key="viewportCopyStrategy"
77
78
  collapsible
78
79
  :default-open="false"
79
80
  >
@@ -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
  }