@arronqzy/vue-view 0.1.9 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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/blueprint-dsl": "1.0.9",
27
- "@arronqzy/i18n": "0.1.7",
28
- "@arronqzy/view-scene3d": "0.1.2",
29
- "@arronqzy/view-table": "0.1.5",
30
- "@arronqzy/vue-rx-store": "0.1.3",
26
+ "@arronqzy/blueprint-dsl": "1.0.10",
31
27
  "@arronqzy/rx-store": "1.0.5",
32
- "@arronqzy/vue-blueprint": "0.1.7"
28
+ "@arronqzy/view-table": "0.1.5",
29
+ "@arronqzy/vue-blueprint": "0.1.8",
30
+ "@arronqzy/i18n": "0.1.8",
31
+ "@arronqzy/view-scene3d": "0.1.2",
32
+ "@arronqzy/vue-rx-store": "0.1.3"
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/typescript-config": "1.0.0",
43
- "@arronqzy/eslint-config": "0.0.0"
42
+ "@arronqzy/eslint-config": "0.0.0",
43
+ "@arronqzy/typescript-config": "1.0.0"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
@@ -85,7 +85,7 @@ const NodeContent = defineComponent({
85
85
  previewMode: p.previewMode,
86
86
  });
87
87
  }
88
- if (el.materialType === "reference") {
88
+ if (el.materialType === "reference" || el.materialType === "viewport") {
89
89
  return h(ReferenceNodeContent, {
90
90
  element: el,
91
91
  allElements: p.allElements,
@@ -145,7 +145,8 @@ const NodeContent = defineComponent({
145
145
  const sortedElements = computed(() => {
146
146
  const byId = new Map<string, PanelElement>();
147
147
  for (const el of props.allElements) byId.set(el.id, el);
148
- return [...props.elements].sort((a, b) => comparePanelElementsPaintOrder(a, b, byId));
148
+ return [...props.elements]
149
+ .sort((a, b) => comparePanelElementsPaintOrder(a, b, byId));
149
150
  });
150
151
 
151
152
  function onSelect(id: string, event: MouseEvent) {
@@ -200,6 +201,7 @@ function hasViewEvent(el: PanelElement) {
200
201
  transform: `rotate(${el.rotate ?? 0}deg)`,
201
202
  transformOrigin: 'center center',
202
203
  boxSizing: 'border-box',
204
+ overflow: el.materialType === 'viewport' ? 'hidden' : undefined,
203
205
  ...getNodeVisualStyle(el),
204
206
  }"
205
207
  @mousedown="onSelect(el.id, $event)"
@@ -157,5 +157,13 @@ const common =
157
157
  />
158
158
  </div>
159
159
  </div>
160
+ <div v-else-if="id === 'viewport'" :class="common">
161
+ <div class="absolute inset-1.5 overflow-hidden rounded-sm border border-primary/70 bg-primary/10">
162
+ <div class="absolute left-1 top-1 h-6 w-8 rounded-[2px] bg-primary/45" />
163
+ <div class="absolute left-6 top-4 h-7 w-10 rounded-[2px] bg-primary/30" />
164
+ <div class="absolute bottom-0 right-0 h-1.5 w-6 rounded-tl bg-primary/55" />
165
+ <div class="absolute bottom-0 right-0 top-2 w-1.5 rounded-l bg-primary/40" />
166
+ </div>
167
+ </div>
160
168
  <div v-else :class="common" />
161
169
  </template>
@@ -157,17 +157,26 @@ function setExpanded(key: string, next: boolean) {
157
157
  }
158
158
 
159
159
  function hasRefInSubtree(node: PanelElement, visited: Set<string>): boolean {
160
- if (node.materialType === "reference") return true;
160
+ if (node.materialType === "reference" || node.materialType === "viewport") return true;
161
161
  if (visited.has(node.id)) return false;
162
162
  const nextVisited = new Set(visited);
163
163
  nextVisited.add(node.id);
164
- const children =
165
- node.refCopyMode === "deep"
166
- ? node.refSnapshot ?? []
164
+ const children = getTreeChildren(node);
165
+ return children.some((child) => hasRefInSubtree(child, nextVisited));
166
+ }
167
+
168
+ function getTreeChildren(node: PanelElement, sourceOverride?: PanelElement[]) {
169
+ if (node.materialType === "reference" || node.materialType === "viewport") {
170
+ return node.refCopyMode === "deep"
171
+ ? node.refSnapshot ?? sourceOverride ?? []
167
172
  : node.refLayerId
168
173
  ? elementsByLayer.value.get(node.refLayerId) ?? []
169
174
  : [];
170
- return children.some((child) => hasRefInSubtree(child, nextVisited));
175
+ }
176
+ if (node.materialType === "grid") {
177
+ return [...(childrenByGridByLayer.value.get(node.layerId)?.get(node.id) ?? [])];
178
+ }
179
+ return [];
171
180
  }
172
181
 
173
182
  function nodeMatchesTreeSearch(node: PanelElement, visited: Set<string>): boolean {
@@ -177,19 +186,7 @@ function nodeMatchesTreeSearch(node: PanelElement, visited: Set<string>): boolea
177
186
  if (visited.has(node.id)) return false;
178
187
  const nextVisited = new Set(visited);
179
188
  nextVisited.add(node.id);
180
- const isRef = node.materialType === "reference";
181
- const isGrid = node.materialType === "grid";
182
- const gridChildren = isGrid
183
- ? [...(childrenByGridByLayer.value.get(node.layerId)?.get(node.id) ?? [])]
184
- : [];
185
- const children = isRef
186
- ? node.refCopyMode === "deep"
187
- ? node.refSnapshot ?? []
188
- : node.refLayerId
189
- ? elementsByLayer.value.get(node.refLayerId) ?? []
190
- : []
191
- : gridChildren;
192
- return children.some((child) => nodeMatchesTreeSearch(child, nextVisited));
189
+ return getTreeChildren(node).some((child) => nodeMatchesTreeSearch(child, nextVisited));
193
190
  }
194
191
 
195
192
  function getRootNodes(layerId: string) {
@@ -38,7 +38,9 @@ const emit = defineEmits<{
38
38
 
39
39
  const nodeKey = computed(() => `node:${props.path}`);
40
40
  const selected = computed(() => props.selectedIds.includes(props.node.id));
41
- const isRef = computed(() => props.node.materialType === "reference");
41
+ const isRef = computed(
42
+ () => props.node.materialType === "reference" || props.node.materialType === "viewport"
43
+ );
42
44
  const refMode = computed(() => props.node.refCopyMode ?? "shallow");
43
45
  const isDeepRef = computed(() => isRef.value && refMode.value === "deep");
44
46
  const nodeHomeLayer = computed(() => props.layerById.get(props.node.layerId));
@@ -82,7 +84,7 @@ function nodeMatchesTreeSearch(
82
84
  const next = new Set(visited);
83
85
  next.add(node.id);
84
86
  const childList =
85
- node.materialType === "reference"
87
+ node.materialType === "reference" || node.materialType === "viewport"
86
88
  ? node.refCopyMode === "deep"
87
89
  ? node.refSnapshot ?? sourceOverride ?? []
88
90
  : node.refLayerId
@@ -22,6 +22,7 @@ import PanelConfigVideoSection from "./config/PanelConfigVideoSection.vue";
22
22
  import PanelConfigGeometrySection from "./config/PanelConfigGeometrySection.vue";
23
23
  import PanelConfigScene3dSection from "./config/PanelConfigScene3dSection.vue";
24
24
  import PanelConfigGridSection from "./config/PanelConfigGridSection.vue";
25
+ import PanelConfigViewportSection from "./config/PanelConfigViewportSection.vue";
25
26
  import PanelConfigGridChildSpan from "./config/PanelConfigGridChildSpan.vue";
26
27
  import PanelConfigReferenceSection from "./config/PanelConfigReferenceSection.vue";
27
28
  import PanelConfigTableSection from "./table/PanelConfigTableSection.vue";
@@ -156,6 +157,9 @@ const visibleSectionCount = computed(() => {
156
157
  if (materialType.value === "image") checks.push(["imageConfig", t("panel.config.sectionImage"), [t("panel.defaults.image")]]);
157
158
  if (materialType.value === "geometry") checks.push(["geometryConfig", t("panel.config.sectionGeometry"), [t("panel.defaults.geometry")]]);
158
159
  if (materialType.value === "grid") checks.push(["gridConfig", t("panel.config.sectionGrid"), [t("panel.config.searchKwGrid")]]);
160
+ if (materialType.value === "viewport") {
161
+ checks.push(["viewportConfig", t("panel.config.sectionViewport"), [t("panel.config.searchKwViewport"), t("panel.config.refLayer")]]);
162
+ }
159
163
  if (props.selectedElement?.parentGridId) {
160
164
  checks.push(["gridChildSpan", t("panel.config.sectionGridChildSpan"), [t("panel.config.searchKwCrossCol")]]);
161
165
  }
@@ -354,6 +358,18 @@ watch(
354
358
  @update:open="(v) => setSectionExpanded('gridConfig', v)"
355
359
  />
356
360
 
361
+ <PanelConfigViewportSection
362
+ v-if="materialType === 'viewport' && shouldShowSection('viewportConfig', t('panel.config.sectionViewport'), [t('panel.config.searchKwViewport'), t('panel.config.viewportOverflow'), t('panel.config.refLayer')])"
363
+ :element="selectedElement"
364
+ :layers="layers"
365
+ :is-editable="isNodeEditable"
366
+ :open="isSectionExpanded('viewportConfig')"
367
+ :force-open="forceOpenSections"
368
+ :set-reference-copy-mode="setReferenceCopyMode"
369
+ :update-element="updateElement"
370
+ @update:open="(v) => setSectionExpanded('viewportConfig', v)"
371
+ />
372
+
357
373
  <PanelConfigGridChildSpan
358
374
  v-if="selectedElement.parentGridId && shouldShowSection('gridChildSpan', t('panel.config.sectionGridChildSpan'), [t('panel.config.searchKwCrossCol'), t('panel.config.searchKwCrossRow')])"
359
375
  :element="selectedElement"
@@ -394,7 +410,7 @@ watch(
394
410
  />
395
411
 
396
412
  <div
397
- v-if="!isChartElement && materialType && !['text', 'table', 'audio', 'video', 'image', 'geometry', 'grid', 'reference'].includes(materialType)"
413
+ v-if="!isChartElement && materialType && !['text', 'table', 'audio', 'video', 'image', 'geometry', 'grid', 'viewport', 'reference'].includes(materialType)"
398
414
  class="text-xs leading-6 text-gray-500"
399
415
  >
400
416
  {{ t("panel.config.notChartType") }}
@@ -78,6 +78,7 @@ export type WorkspaceConfigSidebarProps = Omit<
78
78
  | "lifecyclePhase"
79
79
  | "fetchConfig"
80
80
  | "jsonConfig"
81
+ | "storageConfig"
81
82
  | "logicConfig"
82
83
  | "clockConfig"
83
84
  | "eventConfig"
@@ -4,6 +4,11 @@ import { computed, ref } from "vue";
4
4
  import { Card, Checkbox, Input, InputNumber, Select } from "ant-design-vue";
5
5
  import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
6
6
  import { CHART_TYPES } from "../../utils/chartOptionBuilder";
7
+ import {
8
+ VIEWPORT_OVERFLOW_MODES,
9
+ normalizeViewportOverflow,
10
+ type ViewportOverflowMode,
11
+ } from "../../utils/viewportPlacement";
7
12
 
8
13
  const { t, locale } = useI18n();
9
14
  const props = defineProps<{
@@ -357,7 +362,7 @@ function setNodeCardExpanded(id: string, open: boolean) {
357
362
  </div>
358
363
  </template>
359
364
 
360
- <template v-if="el.materialType === 'reference'">
365
+ <template v-if="el.materialType === 'reference' || el.materialType === 'viewport'">
361
366
  <div class="grid grid-cols-2 gap-2">
362
367
  <label class="block space-y-1">
363
368
  <div>{{ t("panel.config.refLayer") }}</div>
@@ -392,6 +397,30 @@ function setNodeCardExpanded(id: string, open: boolean) {
392
397
  </div>
393
398
  </template>
394
399
 
400
+ <template v-if="el.materialType === 'viewport'">
401
+ <label class="block space-y-1">
402
+ <div>{{ t("panel.config.viewportOverflow") }}</div>
403
+ <Select
404
+ size="small"
405
+ class="w-full"
406
+ :value="normalizeViewportOverflow(el.viewportOverflow)"
407
+ @update:value="(v) => updateElement(el.id, { viewportOverflow: v as ViewportOverflowMode })"
408
+ >
409
+ <Select.Option v-for="mode in VIEWPORT_OVERFLOW_MODES" :key="mode" :value="mode">
410
+ {{
411
+ mode === "scroll-x"
412
+ ? t("panel.config.viewportOverflowScrollX")
413
+ : mode === "scroll-y"
414
+ ? t("panel.config.viewportOverflowScrollY")
415
+ : mode === "scroll"
416
+ ? t("panel.config.viewportOverflowScroll")
417
+ : t("panel.config.viewportOverflowClip")
418
+ }}
419
+ </Select.Option>
420
+ </Select>
421
+ </label>
422
+ </template>
423
+
395
424
  <template v-if="el.materialType === 'image'">
396
425
  <label class="block space-y-1">
397
426
  <div>{{ t("panel.config.backgroundImageUrl") }}</div>
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
3
+ import { Select } from "ant-design-vue";
4
+ import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
5
+ import {
6
+ VIEWPORT_OVERFLOW_MODES,
7
+ normalizeViewportOverflow,
8
+ type ViewportOverflowMode,
9
+ } from "../../utils/viewportPlacement";
10
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
11
+ import ConfigSection from "./ConfigSection.vue";
12
+
13
+ const { t } = useI18n();
14
+ const props = defineProps<{
15
+ element: PanelElement;
16
+ layers: PanelLayer[];
17
+ isEditable: boolean;
18
+ open: boolean;
19
+ forceOpen?: boolean;
20
+ setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
21
+ updateElement: (
22
+ id: string,
23
+ patch: Partial<PanelElement>,
24
+ options?: { batchId?: string; meta?: Record<string, unknown> }
25
+ ) => void;
26
+ }>();
27
+
28
+ const emit = defineEmits<{
29
+ "update:open": [value: boolean];
30
+ }>();
31
+
32
+ const overflowLabelKey: Record<ViewportOverflowMode, string> = {
33
+ "scroll-x": "panel.config.viewportOverflowScrollX",
34
+ "scroll-y": "panel.config.viewportOverflowScrollY",
35
+ scroll: "panel.config.viewportOverflowScroll",
36
+ clip: "panel.config.viewportOverflowClip",
37
+ };
38
+
39
+ function patch(next: Partial<PanelElement>) {
40
+ props.updateElement(props.element.id, next);
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <ConfigSection
46
+ :title="t('panel.config.sectionViewport')"
47
+ :open="open"
48
+ :force-open="forceOpen"
49
+ @update:open="emit('update:open', $event)"
50
+ >
51
+ <template #hint>
52
+ {{ t("panel.config.viewportHint") }}
53
+ </template>
54
+ <ConfigFieldGroup :title="t('panel.config.groupRefSource')">
55
+ <label class="block space-y-1">
56
+ <div>{{ t("panel.config.refLayer") }}</div>
57
+ <Select
58
+ size="small"
59
+ class="w-full"
60
+ :value="element.refLayerId ?? '__none__'"
61
+ :disabled="!isEditable"
62
+ @update:value="(v) => patch({ refLayerId: v === '__none__' ? undefined : String(v) })"
63
+ >
64
+ <Select.Option value="__none__">{{ t("panel.config.noneNoRef") }}</Select.Option>
65
+ <Select.Option
66
+ v-for="layer in layers.filter((l) => l.id !== element.layerId)"
67
+ :key="layer.id"
68
+ :value="layer.id"
69
+ >
70
+ {{ layer.name }}
71
+ </Select.Option>
72
+ </Select>
73
+ </label>
74
+ </ConfigFieldGroup>
75
+ <ConfigFieldGroup
76
+ :title="t('panel.config.groupCopyStrategy')"
77
+ collapsible
78
+ :default-open="false"
79
+ >
80
+ <template #hint>
81
+ {{ t("panel.config.copyStrategyHint") }}
82
+ </template>
83
+ <label class="block space-y-1">
84
+ <div>{{ t("panel.config.copyMode") }}</div>
85
+ <Select
86
+ size="small"
87
+ class="w-full"
88
+ :value="element.refCopyMode ?? 'shallow'"
89
+ :disabled="!isEditable"
90
+ @update:value="(v) => setReferenceCopyMode?.(element.id, v as ReferenceCopyMode)"
91
+ >
92
+ <Select.Option value="shallow">{{ t("panel.config.shallowFollow") }}</Select.Option>
93
+ <Select.Option value="deep">{{ t("panel.config.deepFreeze") }}</Select.Option>
94
+ </Select>
95
+ </label>
96
+ </ConfigFieldGroup>
97
+ <ConfigFieldGroup :title="t('panel.config.groupViewportOverflow')">
98
+ <label class="block space-y-1">
99
+ <div>{{ t("panel.config.viewportOverflow") }}</div>
100
+ <Select
101
+ size="small"
102
+ class="w-full"
103
+ :value="normalizeViewportOverflow(element.viewportOverflow)"
104
+ :disabled="!isEditable"
105
+ @update:value="(v) => patch({ viewportOverflow: v as ViewportOverflowMode })"
106
+ >
107
+ <Select.Option v-for="mode in VIEWPORT_OVERFLOW_MODES" :key="mode" :value="mode">
108
+ {{ t(overflowLabelKey[mode]) }}
109
+ </Select.Option>
110
+ </Select>
111
+ </label>
112
+ </ConfigFieldGroup>
113
+ </ConfigSection>
114
+ </template>
@@ -16,6 +16,12 @@ import type { PanelElement } from "../types";
16
16
  import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
17
17
  import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
18
18
  import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
19
+ import {
20
+ isViewportNode,
21
+ normalizeViewportOverflow,
22
+ viewportOverflowStyle,
23
+ viewportWindowContentScale,
24
+ } from "../utils/viewportPlacement";
19
25
  import TableNodeContent from "./table/TableNodeContent.vue";
20
26
  import Scene3dNodeContent from "@arronqzy/view-scene3d/vue";
21
27
 
@@ -712,15 +718,25 @@ export const ReferenceNodeContent = defineComponent({
712
718
  const sourceHeight = Math.max(1, maxY - minY);
713
719
  const innerW = Math.max(1, p.element.width);
714
720
  const innerH = Math.max(1, p.element.height);
715
- const scale = Math.max(0.05, Math.min(innerW / sourceWidth, innerH / sourceHeight));
716
- const mappedW = sourceWidth * scale;
717
- const mappedH = sourceHeight * scale;
721
+ const windowFit = isViewportNode(p.element);
722
+ const windowScale = windowFit
723
+ ? viewportWindowContentScale(p.element, p.allElements)
724
+ : { scaleX: 1, scaleY: 1 };
725
+ const scaleX = windowFit
726
+ ? windowScale.scaleX
727
+ : Math.max(0.05, Math.min(innerW / sourceWidth, innerH / sourceHeight));
728
+ const scaleY = windowFit ? windowScale.scaleY : scaleX;
729
+ const mappedW = sourceWidth * scaleX;
730
+ const mappedH = sourceHeight * scaleY;
718
731
  return {
719
732
  minX,
720
733
  minY,
721
- scale,
722
- offsetX: (innerW - mappedW) / 2,
723
- offsetY: (innerH - mappedH) / 2,
734
+ scaleX,
735
+ scaleY,
736
+ sourceWidth: mappedW,
737
+ sourceHeight: mappedH,
738
+ offsetX: windowFit ? 0 : (innerW - mappedW) / 2,
739
+ offsetY: windowFit ? 0 : (innerH - mappedH) / 2,
724
740
  };
725
741
  });
726
742
 
@@ -739,20 +755,27 @@ export const ReferenceNodeContent = defineComponent({
739
755
  ]);
740
756
  }
741
757
 
742
- return h("div", { class: "pointer-events-none relative h-full w-full overflow-hidden" }, [
743
- ...sourceNodes.value.map((node) => {
758
+ const overflowCss = isViewportNode(p.element)
759
+ ? viewportOverflowStyle(normalizeViewportOverflow(p.element.viewportOverflow))
760
+ : { overflowX: "hidden" as const, overflowY: "hidden" as const };
761
+ const canScroll =
762
+ isViewportNode(p.element) &&
763
+ (overflowCss.overflowX === "auto" || overflowCss.overflowY === "auto");
764
+
765
+ const innerNodes = sourceNodes.value.map((node) => {
744
766
  const box = getNodeAABB(node);
745
- const left = layout.value!.offsetX + (box.left - layout.value!.minX) * layout.value!.scale;
746
- const top = layout.value!.offsetY + (box.top - layout.value!.minY) * layout.value!.scale;
747
- const width = Math.max(12, node.width * layout.value!.scale);
748
- const height = Math.max(10, node.height * layout.value!.scale);
749
- const boxWidth = Math.max(12, (box.right - box.left) * layout.value!.scale);
750
- const boxHeight = Math.max(10, (box.bottom - box.top) * layout.value!.scale);
767
+ const left = layout.value!.offsetX + (box.left - layout.value!.minX) * layout.value!.scaleX;
768
+ const top = layout.value!.offsetY + (box.top - layout.value!.minY) * layout.value!.scaleY;
769
+ const width = Math.max(12, node.width * layout.value!.scaleX);
770
+ const height = Math.max(10, node.height * layout.value!.scaleY);
771
+ const boxWidth = Math.max(12, (box.right - box.left) * layout.value!.scaleX);
772
+ const boxHeight = Math.max(10, (box.bottom - box.top) * layout.value!.scaleY);
751
773
  return h(
752
774
  "div",
753
775
  {
754
776
  key: node.id,
755
777
  class: "absolute overflow-visible",
778
+ "data-viewport-item": isViewportNode(p.element) ? "true" : undefined,
756
779
  style: { left, top, width: boxWidth, height: boxHeight },
757
780
  },
758
781
  [
@@ -760,6 +783,7 @@ export const ReferenceNodeContent = defineComponent({
760
783
  "div",
761
784
  {
762
785
  class: "absolute",
786
+ "data-viewport-item": isViewportNode(p.element) ? "true" : undefined,
763
787
  style: {
764
788
  left: (boxWidth - width) / 2,
765
789
  top: (boxHeight - height) / 2,
@@ -777,7 +801,7 @@ export const ReferenceNodeContent = defineComponent({
777
801
  previewLayoutKey: p.previewLayoutKey,
778
802
  previewMode: p.previewMode,
779
803
  })
780
- : node.materialType === "reference"
804
+ : node.materialType === "reference" || node.materialType === "viewport"
781
805
  ? h(ReferenceNodeContent as Component, {
782
806
  element: node,
783
807
  allElements: p.allElements,
@@ -800,8 +824,42 @@ export const ReferenceNodeContent = defineComponent({
800
824
  ),
801
825
  ]
802
826
  );
803
- }),
804
- ]);
827
+ });
828
+
829
+ return h(
830
+ "div",
831
+ {
832
+ class: "relative h-full w-full",
833
+ style: {
834
+ overflowX: overflowCss.overflowX,
835
+ overflowY: overflowCss.overflowY,
836
+ pointerEvents: isViewportNode(p.element) ? "auto" : "none",
837
+ },
838
+ onWheel: canScroll
839
+ ? (e: WheelEvent) => {
840
+ e.stopPropagation();
841
+ }
842
+ : undefined,
843
+ },
844
+ [
845
+ h(
846
+ "div",
847
+ {
848
+ class: "pointer-events-none relative",
849
+ "data-viewport-sizer": isViewportNode(p.element) ? "true" : undefined,
850
+ style: isViewportNode(p.element)
851
+ ? {
852
+ width: layout.value.sourceWidth,
853
+ height: layout.value.sourceHeight,
854
+ minWidth: "100%",
855
+ minHeight: "100%",
856
+ }
857
+ : { width: "100%", height: "100%" },
858
+ },
859
+ innerNodes
860
+ ),
861
+ ]
862
+ );
805
863
  };
806
864
  },
807
865
  }) as Component;
@@ -26,6 +26,7 @@ const MATERIAL_LABEL_KEYS: Record<string, string> = {
26
26
  text: "panel.material.text",
27
27
  rect: "panel.material.rect",
28
28
  grid: "panel.material.grid",
29
+ viewport: "panel.material.viewport",
29
30
  image: "panel.material.image",
30
31
  video: "panel.material.video",
31
32
  audio: "panel.material.audio",
@@ -68,6 +69,7 @@ export function getDefaultCategories(t: TranslateFn): MaterialCategory[] {
68
69
  { id: "geometry", title: t("panel.material.geometry") },
69
70
  { id: "scene3d", title: t("panel.material.scene3d") },
70
71
  { id: "grid", title: t("panel.material.grid") },
72
+ { id: "viewport", title: t("panel.material.viewport") },
71
73
  { id: "image", title: t("panel.material.image") },
72
74
  { id: "reference", title: t("panel.material.reference") },
73
75
  ],
@@ -16,6 +16,7 @@ import {
16
16
  DEFAULT_LAYER_ID,
17
17
  getDefaultChartConfig,
18
18
  getDefaultGridConfig,
19
+ getDefaultViewportConfig,
19
20
  getDefaultNodeName,
20
21
  getDefaultScene3dConfigForMaterial,
21
22
  getDefaultSizeByMaterial,
@@ -569,7 +570,10 @@ export function usePanelElements() {
569
570
  .filter((n) => isPanelElementNode(n) && n.props)
570
571
  .map((n) => n.props as PanelElement);
571
572
  const desiredMode = options?.referenceCopyMode;
572
- if (nextProps.materialType === "reference" && desiredMode) {
573
+ if (
574
+ (nextProps.materialType === "reference" || nextProps.materialType === "viewport") &&
575
+ desiredMode
576
+ ) {
573
577
  if (desiredMode === "deep" && nextProps.refLayerId) {
574
578
  nextProps.refCopyMode = "deep";
575
579
  nextProps.refSnapshot = buildDeepReferenceSnapshot(all, nextProps.refLayerId);
@@ -611,7 +615,9 @@ export function usePanelElements() {
611
615
  name: getDefaultNodeName(materialType, t),
612
616
  ...getDefaultTextContent(materialType, t),
613
617
  ...getDefaultGridConfig(materialType),
614
- refCopyMode: materialType === "reference" ? "shallow" : undefined,
618
+ ...getDefaultViewportConfig(materialType),
619
+ refCopyMode:
620
+ materialType === "reference" || materialType === "viewport" ? "shallow" : undefined,
615
621
  chart: getDefaultChartConfig(materialType, t),
616
622
  table: getDefaultTableConfig(materialType, t),
617
623
  geometryShape: materialType === "geometry" ? "rect" : undefined,
@@ -712,7 +718,7 @@ export function usePanelElements() {
712
718
  .filter((n) => isPanelElementNode(n) && n.props)
713
719
  .map((n) => n.props as PanelElement);
714
720
  const target = all.find((el) => el.id === id);
715
- if (!target || target.materialType !== "reference") return;
721
+ if (!target || (target.materialType !== "reference" && target.materialType !== "viewport")) return;
716
722
  const currentLayers =
717
723
  (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
718
724
  const layer = currentLayers.find((l) => l.id === target.layerId);
@@ -904,7 +910,7 @@ export function usePanelElements() {
904
910
  if (target.isMapping) return false;
905
911
  const willBeDeleted = mode === "remove" && el.layerId === layerId;
906
912
  if (willBeDeleted) return false;
907
- if (el.materialType !== "reference") return false;
913
+ if (el.materialType !== "reference" && el.materialType !== "viewport") return false;
908
914
  if (el.refLayerId !== layerId) return false;
909
915
  return (el.refCopyMode ?? "shallow") !== "deep";
910
916
  });
@@ -54,6 +54,8 @@ export type PanelElementStyle = {
54
54
 
55
55
  export type ReferenceCopyMode = "shallow" | "deep";
56
56
 
57
+ export type ViewportOverflowMode = "scroll-x" | "scroll-y" | "scroll" | "clip";
58
+
57
59
  export type { PanelTableConfig } from "@arronqzy/view-table";
58
60
  import type { PanelTableConfig } from "@arronqzy/view-table";
59
61
  import type { Scene3dConfig } from "@arronqzy/view-scene3d";
@@ -84,6 +86,8 @@ export type PanelElement = {
84
86
  gridPadding?: number;
85
87
  gridSnapThreshold?: number;
86
88
  parentGridId?: string;
89
+ /** 视窗节点:只显示自身尺寸内的引用图层内容,超出部分滚动或裁剪 */
90
+ viewportOverflow?: ViewportOverflowMode;
87
91
  gridSlotIndex?: number;
88
92
  gridColSpan?: number;
89
93
  gridRowSpan?: number;
@@ -15,6 +15,7 @@ const DEFAULT_NODE_NAME_KEYS: Record<string, string> = {
15
15
  funnel: "panel.defaults.funnel",
16
16
  text: "panel.defaults.text",
17
17
  grid: "panel.defaults.grid",
18
+ viewport: "panel.defaults.viewport",
18
19
  image: "panel.defaults.image",
19
20
  video: "panel.defaults.video",
20
21
  audio: "panel.defaults.audio",
@@ -71,6 +72,8 @@ export function getDefaultSizeByMaterial(materialType: string) {
71
72
  return { width: 180, height: 120 };
72
73
  case "grid":
73
74
  return { width: 320, height: 220 };
75
+ case "viewport":
76
+ return { width: 360, height: 240 };
74
77
  case "image":
75
78
  return { width: 220, height: 140 };
76
79
  case "video":
@@ -145,6 +148,13 @@ export function getDefaultGridConfig(materialType: string) {
145
148
  } as const;
146
149
  }
147
150
 
151
+ export function getDefaultViewportConfig(materialType: string) {
152
+ if (materialType !== "viewport") return {};
153
+ return {
154
+ viewportOverflow: "clip" as const,
155
+ };
156
+ }
157
+
148
158
  export function getDefaultChartConfig(
149
159
  materialType: string,
150
160
  t: TranslateFn = tForLocale("zh-CN")
@@ -26,9 +26,11 @@ export function buildDeepReferenceSnapshot(
26
26
  const layerNodes = allElements.filter((el) => el.layerId === layerId);
27
27
  return layerNodes.map((el) => {
28
28
  const cloned = clonePanelElement(el);
29
- if (cloned.materialType === "reference" && cloned.refLayerId) {
30
- cloned.refCopyMode = "deep";
31
- cloned.refSnapshot = buildDeepReferenceSnapshot(allElements, cloned.refLayerId, nextVisited);
29
+ if (cloned.materialType === "reference" || cloned.materialType === "viewport") {
30
+ if (cloned.refLayerId) {
31
+ cloned.refCopyMode = "deep";
32
+ cloned.refSnapshot = buildDeepReferenceSnapshot(allElements, cloned.refLayerId, nextVisited);
33
+ }
32
34
  }
33
35
  return cloned;
34
36
  });
@@ -0,0 +1,73 @@
1
+ export const VIEWPORT_OVERFLOW_MODES = [
2
+ "scroll-x",
3
+ "scroll-y",
4
+ "scroll",
5
+ "clip",
6
+ ] as const;
7
+
8
+ export type ViewportOverflowMode = (typeof VIEWPORT_OVERFLOW_MODES)[number];
9
+
10
+ export const DEFAULT_VIEWPORT_OVERFLOW: ViewportOverflowMode = "clip";
11
+
12
+ export function isViewportOverflowMode(
13
+ value: unknown
14
+ ): value is ViewportOverflowMode {
15
+ return (
16
+ value === "scroll-x" ||
17
+ value === "scroll-y" ||
18
+ value === "scroll" ||
19
+ value === "clip"
20
+ );
21
+ }
22
+
23
+ export function normalizeViewportOverflow(
24
+ value: unknown
25
+ ): ViewportOverflowMode {
26
+ return isViewportOverflowMode(value) ? value : DEFAULT_VIEWPORT_OVERFLOW;
27
+ }
28
+
29
+ export function viewportOverflowStyle(mode: ViewportOverflowMode): {
30
+ overflowX: "hidden" | "auto";
31
+ overflowY: "hidden" | "auto";
32
+ } {
33
+ switch (mode) {
34
+ case "scroll-x":
35
+ return { overflowX: "auto", overflowY: "hidden" };
36
+ case "scroll-y":
37
+ return { overflowX: "hidden", overflowY: "auto" };
38
+ case "scroll":
39
+ return { overflowX: "auto", overflowY: "auto" };
40
+ default:
41
+ return { overflowX: "hidden", overflowY: "hidden" };
42
+ }
43
+ }
44
+
45
+ export function isViewportNode(
46
+ el: { materialType?: string } | undefined
47
+ ): boolean {
48
+ return el?.materialType === "viewport";
49
+ }
50
+
51
+ /** 引用节点与视窗节点都通过选择图层来预览内容 */
52
+ export function isLayerPreviewNode(
53
+ el: { materialType?: string } | undefined
54
+ ): boolean {
55
+ return el?.materialType === "reference" || el?.materialType === "viewport";
56
+ }
57
+
58
+ /**
59
+ * 视窗按自身宽高开窗;预览里节点会被整体拉满视口,
60
+ * 内部引用内容必须用同一套缩放,滚动比例才和编辑态一致。
61
+ */
62
+ export function viewportWindowContentScale(
63
+ current: { id: string; width: number; height: number },
64
+ originals: ReadonlyArray<{ id: string; width: number; height: number }>
65
+ ): { scaleX: number; scaleY: number } {
66
+ const original = originals.find((n) => n.id === current.id);
67
+ const ow = Math.max(1, original?.width ?? current.width);
68
+ const oh = Math.max(1, original?.height ?? current.height);
69
+ return {
70
+ scaleX: Math.max(1, current.width) / ow,
71
+ scaleY: Math.max(1, current.height) / oh,
72
+ };
73
+ }