+}
+
+export interface IDesignerLayoutContext {
+ theme?: 'dark' | 'light' | (string & {})
+ prefixCls: string
+ position?: 'fixed' | 'absolute' | 'relative'
+}
+
+export interface IWorkspaceContext {
+ id: string
+ title?: string
+ description?: string
+}
+
+export type DnFC = DefineComponent
& {
+ Resource?: IResource[]
+ Behavior?: IBehavior[]
+}
+
+export type DnComponent
= DefineComponent
& {
+ Resource?: IResource[]
+ Behavior?: IBehavior[]
+}
diff --git a/packages/designable/src/variables.less b/packages/designable/src/variables.less
new file mode 100644
index 0000000..2d10112
--- /dev/null
+++ b/packages/designable/src/variables.less
@@ -0,0 +1,6 @@
+// @import '~ant-design-vue/lib/style/themes/index.less';
+
+@prefix-cls: ~'dn';
+@border-color-split: #f0f0f0;
+@text-color-secondary: #666666;
+@primary-color: #999999;
diff --git a/packages/designable/src/widgets/AuxToolWidget/Copy.tsx b/packages/designable/src/widgets/AuxToolWidget/Copy.tsx
new file mode 100644
index 0000000..cc163c5
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/Copy.tsx
@@ -0,0 +1,40 @@
+// import React from 'react'
+import type { TreeNode } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { Button } from 'ant-design-vue'
+import type { PropType } from 'vue'
+import { defineComponent } from 'vue'
+import { useOperation, usePrefix } from '../../hooks'
+import { useStyle } from '../../shared/util'
+import { IconWidget } from '../IconWidget'
+
+export interface ICopyProps {
+ node: TreeNode
+}
+
+const CopyComponent = defineComponent({
+ name: 'DnCopy',
+ props: { node: { type: Object as PropType } },
+ setup(props) {
+ const operationRef = useOperation()
+ const prefixRef = usePrefix('aux-copy')
+ const style = useStyle()
+ return () => {
+ if (props.node === props.node.root) return null
+ return (
+
+ )
+ }
+ }
+})
+
+export const Copy = composeExport(CopyComponent, { displayName: 'Copy' })
diff --git a/packages/designable/src/widgets/AuxToolWidget/Cover.tsx b/packages/designable/src/widgets/AuxToolWidget/Cover.tsx
new file mode 100644
index 0000000..7620f61
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/Cover.tsx
@@ -0,0 +1,95 @@
+import type { TreeNode } from '@pind/designable-core'
+import { ClosestPosition, CursorStatus } from '@pind/designable-core'
+import { isNum } from '@pind/designable-shared'
+import { observer } from '@formily/reactive-vue'
+import { FragmentComponent as Fragment } from '@formily/vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent, toRef } from 'vue'
+import {
+ useCursor,
+ useMoveHelper,
+ usePrefix,
+ useValidNodeOffsetRect,
+ useViewport
+} from '../../hooks'
+
+export interface ICoverRectProps {
+ node: TreeNode
+ dragging?: boolean
+ dropping?: boolean
+}
+
+const CoverRect = defineComponent({
+ name: 'DnCoverRect',
+ props: ['dragging', 'dropping', 'node'],
+ setup(props) {
+ const prefixRef = usePrefix('aux-cover-rect')
+ const node = toRef(props, 'node')
+ const rectRef = useValidNodeOffsetRect(node)
+
+ return () => {
+ const rect = rectRef.value
+ const createCoverStyle = () => {
+ const baseStyle: any = {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ pointerEvents: 'none'
+ }
+ if (rect) {
+ baseStyle.transform = `perspective(1px) translate3d(${rect.x}px,${rect.y}px,0)`
+ baseStyle.height = isNum(rect.height) ? rect.height + 'px' : rect.height
+ baseStyle.width = isNum(rect.width) ? rect.width + 'px' : rect.width
+ }
+ return baseStyle
+ }
+ return (
+
+ )
+ }
+ }
+})
+
+const CoverComponent = observer(
+ defineComponent({
+ name: 'DnCover',
+ setup() {
+ const moveHelperRef = useMoveHelper()
+ const viewportRef = useViewport()
+ const cursorRef = useCursor()
+ const renderDropCover = () => {
+ if (
+ !moveHelperRef.value.closestNode ||
+ !moveHelperRef.value.closestNode?.allowAppend(moveHelperRef.value.dragNodes) ||
+ moveHelperRef.value.closestDirection !== ClosestPosition.Inner
+ )
+ return null
+ return
+ }
+ return () => {
+ if (cursorRef.value.status !== CursorStatus.Dragging) return null
+ return (
+
+ {moveHelperRef.value.dragNodes.map((node) => {
+ if (!node) return
+ if (!viewportRef.value.findElementById(node.id)) return
+ return
+ })}
+ {renderDropCover()}
+
+ )
+ }
+ }
+ })
+)
+
+export const Cover = composeExport(CoverComponent, { displayName: 'Cover' })
diff --git a/packages/designable/src/widgets/AuxToolWidget/DashedBox.tsx b/packages/designable/src/widgets/AuxToolWidget/DashedBox.tsx
new file mode 100644
index 0000000..3551577
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/DashedBox.tsx
@@ -0,0 +1,75 @@
+import { computed, defineComponent } from 'vue'
+import { isNum } from '@pind/designable-shared'
+import { observer } from '@formily/reactive-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { useHover, usePrefix, useValidNodeOffsetRect, useSelection } from '../../hooks'
+
+const DashBox = observer(
+ defineComponent({
+ name: 'DnDashBox',
+ setup() {
+ const hoverRef = useHover()
+ const prefixRef = usePrefix('aux-dashed-box')
+ const selectionRef = useSelection()
+
+ const node = computed(() => hoverRef.value?.node)
+
+ const rectRef = useValidNodeOffsetRect(node)
+
+ return () => {
+ const rect = rectRef.value
+ const createTipsStyle = () => {
+ const baseStyle: any = {
+ top: 0,
+ left: 0,
+ pointerEvents: 'none',
+ boxSizing: 'border-box',
+ visibility: 'hidden',
+ zIndex: 2
+ }
+ if (rect) {
+ baseStyle.transform = `perspective(1px) translate3d(${rect.x}px,${rect.y}px,0)`
+ baseStyle.height = isNum(rect.height) ? rect.height + 'px' : rect.height
+ baseStyle.width = isNum(rect.width) ? rect.width + 'px' : rect.width
+ baseStyle.visibility = 'visible'
+ }
+ return baseStyle
+ }
+ if (!hoverRef.value.node) return null
+ if (hoverRef.value.node.hidden) return null
+ if (selectionRef.value.selected.includes(hoverRef.value.node.id)) return null
+ return (
+
+
+ {hoverRef.value?.node.getMessage('title')}
+
+
+ )
+ }
+ }
+ })
+)
+
+export const DashedBox = composeExport(
+ observer(
+ defineComponent({
+ name: 'DnDashedBox',
+ setup() {
+ const hoverRef = useHover()
+ return () =>
+ }
+ })
+ ),
+ { displayName: 'DashedBox' }
+)
diff --git a/packages/designable/src/widgets/AuxToolWidget/Delete.tsx b/packages/designable/src/widgets/AuxToolWidget/Delete.tsx
new file mode 100644
index 0000000..3d5e4a0
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/Delete.tsx
@@ -0,0 +1,35 @@
+import type { TreeNode } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { Button } from 'ant-design-vue'
+import type { PropType } from 'vue'
+import { defineComponent } from 'vue'
+import { useOperation, usePrefix } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+
+export interface IDeleteProps {
+ node: TreeNode
+}
+
+const DeleteComponent = defineComponent({
+ name: 'DnDelete',
+ props: { node: { type: Object as PropType } },
+ setup(props) {
+ const operationRef = useOperation()
+ const prefixRef = usePrefix('aux-copy')
+ return () => {
+ if (props.node === props.node.root) return null
+ return (
+
+ )
+ }
+ }
+})
+export const Delete = composeExport(DeleteComponent, { displayName: 'Delete' })
diff --git a/packages/designable/src/widgets/AuxToolWidget/DragHandler.tsx b/packages/designable/src/widgets/AuxToolWidget/DragHandler.tsx
new file mode 100644
index 0000000..74aa159
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/DragHandler.tsx
@@ -0,0 +1,42 @@
+import type { TreeNode } from '@pind/designable-core'
+import { observer } from '@formily/reactive-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { Button } from 'ant-design-vue'
+import type { PropType } from 'vue'
+import { defineComponent } from 'vue'
+import { useDesigner, usePrefix } from '../../hooks'
+import { useStyle } from '../../shared'
+import { IconWidget } from '../IconWidget'
+
+export interface IDragHandlerProps {
+ node: TreeNode
+}
+
+const DragHandlerComponent = observer(
+ defineComponent({
+ name: 'DnDragHandler',
+ props: { node: { type: Object as PropType } },
+ setup(props) {
+ const designerRef = useDesigner()
+ const style = useStyle()
+ const prefixRef = usePrefix('aux-drag-handler')
+
+ return () => {
+ const node = props.node!
+ if (node === node.root || !node.allowDrag()) return null
+ const handlerProps = {
+ [designerRef.value.props.nodeDragHandlerAttrName!]: 'true'
+ }
+ return (
+
+ )
+ }
+ }
+ })
+)
+
+export const DragHandler = composeExport(DragHandlerComponent, {
+ displayName: 'DragHandler'
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/FreeSelection.tsx b/packages/designable/src/widgets/AuxToolWidget/FreeSelection.tsx
new file mode 100644
index 0000000..a62291c
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/FreeSelection.tsx
@@ -0,0 +1,61 @@
+import { observer } from '@formily/reactive-vue'
+import { CursorDragType, CursorStatus } from '@pind/designable-core'
+import { calcRectByStartEndPoint, isNum } from '@pind/designable-shared'
+import { defineComponent } from 'vue'
+import { useCursor, useOperation, usePrefix, useViewport } from '../../hooks'
+
+export const FreeSelection = observer(
+ defineComponent({
+ name: 'DnFreeSelection',
+ props: [],
+ setup() {
+ const operationRef = useOperation()
+ const cursorRef = useCursor()
+ const viewportRef = useViewport()
+ const prefixRef = usePrefix('aux-free-selection')
+ const createSelectionStyle = () => {
+ const cursor = cursorRef.value
+ const viewport = viewportRef.value
+ const startDragPoint = viewportRef.value.getOffsetPoint({
+ x: cursor.dragStartPosition.topClientX,
+ y: cursor.dragStartPosition.topClientY
+ })
+ const currentPoint = viewportRef.value.getOffsetPoint({
+ x: cursor.position.topClientX,
+ y: cursor.position.topClientY
+ })
+ const rect = calcRectByStartEndPoint(
+ startDragPoint,
+ currentPoint,
+ viewport.dragScrollXDelta,
+ viewport.dragScrollYDelta
+ )
+ const baseStyle: any = {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ opacity: 0.2,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ transform: `perspective(1px) translate3d(${rect.x}px,${rect.y}px,0)`,
+ height: isNum(rect.height) ? rect.height + 'px' : rect.height,
+ width: isNum(rect.width) ? rect.width + 'px' : rect.width,
+ pointerEvents: 'none',
+ boxSizing: 'border-box',
+ zIndex: 1
+ }
+ return baseStyle
+ }
+ return () => {
+ const cursor = cursorRef.value
+ if (
+ operationRef.value.moveHelper.hasDragNodes ||
+ cursor.status !== CursorStatus.Dragging ||
+ cursor.type !== CursorDragType.Move
+ )
+ return null
+ return
+ }
+ }
+ })
+)
diff --git a/packages/designable/src/widgets/AuxToolWidget/Helpers.tsx b/packages/designable/src/widgets/AuxToolWidget/Helpers.tsx
new file mode 100644
index 0000000..3887b6f
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/Helpers.tsx
@@ -0,0 +1,136 @@
+import type { TreeNode } from '@pind/designable-core'
+import { reaction } from '@formily/reactive'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { Button } from 'ant-design-vue'
+import { defineComponent, nextTick, ref } from 'vue'
+import { usePrefix, useViewport } from '../../hooks'
+import { useEffect } from '../../shared/useEffect'
+import { Copy } from './Copy'
+import { Delete } from './Delete'
+import { DragHandler } from './DragHandler'
+import { Selector } from './Selector'
+
+const HELPER_DEBOUNCE_TIMEOUT = 100
+
+export interface IHelpersProps {
+ node: TreeNode
+ nodeRect: DOMRect
+}
+
+export interface IViewportState {
+ viewportWidth?: number
+ viewportHeight?: number
+ viewportScrollX?: number
+ viewportScrollY?: number
+ viewportIsScrollTop?: boolean
+ viewportIsScrollBottom?: boolean
+}
+
+const HelpersComponent = defineComponent({
+ props: ['node', 'nodeRect'],
+ setup(props) {
+ const prefixRef = usePrefix('aux-helpers')
+ const viewportRef = useViewport()
+ const unmountRef = ref(false)
+ const refContainer = ref()
+ const positionRef = ref('top-right')
+
+ useEffect(
+ () => {
+ let request = null
+ const getYInViewport = (nodeRect: DOMRect, helpersRect: DOMRect) => {
+ if (nodeRect.top - viewportRef.value.scrollY > helpersRect.height) {
+ return 'top'
+ } else if (
+ viewportRef.value.isScrollTop &&
+ nodeRect.height + helpersRect.height > viewportRef.value.height
+ ) {
+ return 'inner-top'
+ } else if (
+ viewportRef.value.isScrollBottom &&
+ nodeRect.height + helpersRect.height > viewportRef.value.height
+ ) {
+ return 'inner-bottom'
+ }
+
+ return 'bottom'
+ }
+
+ const getXInViewport = (nodeRect: DOMRect, helpersRect: DOMRect) => {
+ const widthDelta = helpersRect.width - nodeRect.width
+ if (widthDelta >= 0) {
+ if (nodeRect.x < widthDelta) {
+ return 'left'
+ } else if (nodeRect.right + widthDelta > viewportRef.value.width) {
+ return 'right'
+ } else {
+ return 'center'
+ }
+ }
+ return 'right'
+ }
+
+ const update = () => {
+ const nodeRect = props.nodeRect
+ const ref = refContainer
+ const helpersRect = ref.value?.getBoundingClientRect()
+ if (!helpersRect || !nodeRect) return
+ if (unmountRef.value) return
+ positionRef.value =
+ getYInViewport(nodeRect, helpersRect) + '-' + getXInViewport(nodeRect, helpersRect)
+ }
+
+ nextTick(() => {
+ update()
+ })
+
+ return reaction(
+ () => [
+ viewportRef.value.width,
+ viewportRef.value.height,
+ viewportRef.value.scrollX,
+ viewportRef.value.scrollY,
+ viewportRef.value.isScrollBottom,
+ viewportRef.value.isScrollTop
+ ],
+ () => {
+ clearTimeout(request)
+ request = setTimeout(update, HELPER_DEBOUNCE_TIMEOUT)
+ }
+ )
+ },
+ () => [viewportRef.value, props.nodeRect]
+ )
+
+ return () => {
+ const node = props.node
+ const nodeRect = props.nodeRect
+ if (!nodeRect || !node) return null
+
+ return (
+
+
+
+
+ {node?.allowClone() === false ? null : }
+ {node?.allowDrag() === false ? null : }
+ {node?.allowDelete() === false ? null : }
+
+
+
+ )
+ }
+ }
+})
+
+export const Helpers = composeExport(HelpersComponent, {
+ displayName: 'Helpers'
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/Insertion.tsx b/packages/designable/src/widgets/AuxToolWidget/Insertion.tsx
new file mode 100644
index 0000000..cca609b
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/Insertion.tsx
@@ -0,0 +1,95 @@
+import { ClosestPosition } from '@pind/designable-core'
+import { observer } from '@formily/reactive-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent } from 'vue'
+import { isNum } from '@pind/designable-shared'
+import { useMoveHelper, usePrefix } from '../../hooks'
+
+export const InsertionComponent = observer(
+ defineComponent({
+ props: [],
+ setup() {
+ const moveHelper = useMoveHelper()
+ const prefix = usePrefix('aux-insertion')
+
+ const createInsertionStyle = (): any => {
+ const closestDirection = moveHelper.value.closestDirection
+ const closestRect = moveHelper.value.viewportClosestOffsetRect
+ const isInlineLayout = moveHelper.value.closestNode?.moveLayout === 'horizontal'
+ const baseStyle: any = {
+ position: 'absolute',
+ transform: 'perspective(1px) translate3d(0,0,0)',
+ top: 0,
+ left: 0
+ }
+ if (!closestRect) return baseStyle
+ if (
+ closestDirection === ClosestPosition.Before ||
+ closestDirection === ClosestPosition.ForbidBefore
+ ) {
+ baseStyle.width = 2
+ baseStyle.height = closestRect.height
+ baseStyle.transform = `perspective(1px) translate3d(${closestRect.x}px,${closestRect.y}px,0)`
+ } else if (
+ closestDirection === ClosestPosition.After ||
+ closestDirection === ClosestPosition.ForbidAfter
+ ) {
+ baseStyle.width = 2
+ baseStyle.height = closestRect.height
+ baseStyle.transform = `perspective(1px) translate3d(${
+ closestRect.x + closestRect.width - 2
+ }px,${closestRect.y}px,0)`
+ } else if (
+ closestDirection === ClosestPosition.InnerAfter ||
+ closestDirection === ClosestPosition.Under ||
+ closestDirection === ClosestPosition.ForbidInnerAfter ||
+ closestDirection === ClosestPosition.ForbidUnder
+ ) {
+ if (isInlineLayout) {
+ baseStyle.width = 2
+ baseStyle.height = closestRect.height
+ baseStyle.transform = `perspective(1px) translate3d(${
+ closestRect.x + closestRect.width - 2
+ }px,${closestRect.y}px,0)`
+ } else {
+ baseStyle.width = closestRect.width
+ baseStyle.height = 2
+ baseStyle.transform = `perspective(1px) translate3d(${closestRect.x}px,${
+ closestRect.y + closestRect.height - 2
+ }px,0)`
+ }
+ } else if (
+ closestDirection === ClosestPosition.InnerBefore ||
+ closestDirection === ClosestPosition.Upper ||
+ closestDirection === ClosestPosition.ForbidInnerBefore ||
+ closestDirection === ClosestPosition.ForbidUpper
+ ) {
+ if (isInlineLayout) {
+ baseStyle.width = 2
+ baseStyle.height = closestRect.height
+ baseStyle.transform = `perspective(1px) translate3d(${closestRect.x}px,${closestRect.y}px,0)`
+ } else {
+ baseStyle.width = closestRect.width
+ baseStyle.height = 2
+ baseStyle.transform = `perspective(1px) translate3d(${closestRect.x}px,${closestRect.y}px,0)`
+ }
+ }
+ if (closestDirection.includes('FORBID')) {
+ baseStyle.backgroundColor = 'red'
+ }
+ Object.keys(baseStyle).forEach((key) => {
+ const value = baseStyle[key]
+ isNum(value) && (baseStyle[key] = value + 'px')
+ })
+ return baseStyle
+ }
+ return () => {
+ return
+ }
+ }
+ })
+)
+
+export const Insertion = composeExport(InsertionComponent, {
+ displayName: 'Insertion'
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/ResizeHandler.tsx b/packages/designable/src/widgets/AuxToolWidget/ResizeHandler.tsx
new file mode 100644
index 0000000..0273c67
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/ResizeHandler.tsx
@@ -0,0 +1,48 @@
+import type { TreeNode } from '@pind/designable-core'
+import { FragmentComponent } from '@formily/vue'
+import { defineComponent } from 'vue'
+import { useDesigner, usePrefix } from '../../hooks'
+
+export interface IResizeHandlerProps {
+ node: TreeNode
+}
+
+export const ResizeHandler = defineComponent({
+ name: 'DnResizeHandler',
+ props: ['node'],
+ setup(props) {
+ const designerRef = useDesigner()
+ const prefixRef = usePrefix('aux-node-resize-handler')
+
+ return () => {
+ const allowResize = props.node.allowResize()
+ if (!allowResize) return null
+ const allowX = allowResize.includes('x')
+ const allowY = allowResize.includes('y')
+
+ const createHandlerProps = (value: string) => {
+ return {
+ [designerRef.value.props.nodeResizeHandlerAttrName!]: value,
+ class: {
+ [prefixRef.value]: true,
+ [value]: true
+ }
+ }
+ }
+ const handlerProps = {
+ xStart: createHandlerProps('x-start'),
+ xEnd: createHandlerProps('x-end'),
+ yStart: createHandlerProps('y-start'),
+ yEnd: createHandlerProps('y-end')
+ }
+ return (
+
+ {allowX && }
+ {allowX && }
+ {allowY && }
+ {allowY && }
+
+ )
+ }
+ }
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/Selection.tsx b/packages/designable/src/widgets/AuxToolWidget/Selection.tsx
new file mode 100644
index 0000000..6a2f2f6
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/Selection.tsx
@@ -0,0 +1,105 @@
+import { observer } from '@formily/reactive-vue'
+import { FragmentComponent as Fragment } from '@formily/vue'
+import type { TreeNode } from '@pind/designable-core'
+import { isNum } from '@pind/designable-shared'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent, toRef } from 'vue'
+import {
+ useCursor,
+ useDesigner,
+ useMoveHelper,
+ usePrefix,
+ useSelection,
+ useTree,
+ useValidNodeOffsetRect
+} from '../../hooks'
+import { Helpers } from './Helpers'
+import { ResizeHandler } from './ResizeHandler'
+import { TranslateHandler } from './TranslateHandler'
+
+export interface ISelectionBoxProps {
+ node: TreeNode
+ showHelpers: boolean
+}
+
+export const SelectionBox = defineComponent({
+ name: 'DnSelectionBox',
+ inheritAttrs: false,
+ props: ['node', 'showHelpers'],
+ setup(props, { attrs }) {
+ const designerRef = useDesigner()
+ const prefixRef = usePrefix('aux-selection-box')
+ const innerPrefixRef = usePrefix('aux-selection-box-inner')
+ const node = toRef(props, 'node')
+
+ const nodeRectRef = useValidNodeOffsetRect(node)
+
+ return () => {
+ const nodeRect = nodeRectRef.value
+ const createSelectionStyle = () => {
+ const baseStyle: Record = {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ pointerEvents: 'none',
+ boxSizing: 'border-box'
+ }
+ if (nodeRect) {
+ baseStyle.transform = `perspective(1px) translate3d(${nodeRect.x}px,${nodeRect.y}px,0)`
+ baseStyle.height = isNum(nodeRect.height) ? nodeRect.height + 'px' : nodeRect.height
+ baseStyle.width = isNum(nodeRect.width) ? nodeRect.width + 'px' : nodeRect.width
+ }
+ return baseStyle
+ }
+ if (!nodeRect) return null
+ if (!nodeRect.width || !nodeRect.height) return null
+
+ const selectionId = {
+ [designerRef.value.props.nodeSelectionIdAttrName!]: props.node.id
+ }
+ return (
+
+
+
+
+ {props.showHelpers &&
}
+
+ )
+ }
+ }
+})
+
+const SelectionComponent = observer(
+ defineComponent({
+ name: 'DnSelection',
+ setup() {
+ const selectionRef = useSelection()
+ const treeRef = useTree()
+ const cursorRef = useCursor()
+ const viewportDragonRef = useMoveHelper()
+ return () => {
+ if (cursorRef.value.status !== 'NORMAL' && viewportDragonRef.value.touchNode) return null
+ return (
+
+ {selectionRef.value.selected.map((id) => {
+ const node = treeRef.value.findById(id)
+ if (!node) return
+ if (node.hidden) return
+ return (
+
+ )
+ })}
+
+ )
+ }
+ }
+ })
+)
+
+export const Selection = composeExport(SelectionComponent, {
+ displayName: 'Selection'
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/Selector.tsx b/packages/designable/src/widgets/AuxToolWidget/Selector.tsx
new file mode 100644
index 0000000..442c66b
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/Selector.tsx
@@ -0,0 +1,146 @@
+import type { TreeNode } from '@pind/designable-core'
+import { observer } from '@formily/reactive-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { Button } from 'ant-design-vue'
+import type { PropType, Ref } from 'vue'
+import { defineComponent, onBeforeUnmount, onMounted, ref, unref } from 'vue'
+import { useHover, usePrefix, useSelection } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+import { NodeTitleWidget } from '../NodeTitleWidget'
+
+const useMouseHover = >(
+ refInstance: T,
+ enter?: () => void,
+ leave?: () => void
+) => {
+ const unmounted = ref(false)
+ let timer = null
+
+ const onMouseOver = (e: MouseEvent) => {
+ const target: HTMLElement = e.target as any
+ clearTimeout(timer)
+ timer = setTimeout(() => {
+ if (unmounted.value) return
+ const result = unref(refInstance) as any
+ if (result?.contains(target)) {
+ enter && enter()
+ } else {
+ leave && leave()
+ }
+ }, 100)
+ }
+
+ onMounted(() => {
+ document.addEventListener('mouseover', onMouseOver)
+ })
+
+ onBeforeUnmount(() => {
+ unmounted.value = true
+ document.removeEventListener('mouseover', onMouseOver)
+ })
+}
+
+export interface ISelectorProps {
+ node: TreeNode
+}
+
+const SelectorComponent = observer(
+ defineComponent({
+ name: 'DnSelector',
+ props: { node: { type: Object as PropType } },
+ setup(props) {
+ const expand = ref(false)
+ const setExpand = (value) => {
+ expand.value = value
+ }
+
+ const hoverRef = useHover()
+ const refInstance = ref()
+ const selectionRef = useSelection()
+ const prefixRef = usePrefix('aux-selector')
+
+ useMouseHover(
+ refInstance,
+ () => {
+ setExpand(true)
+ },
+ () => {
+ setExpand(false)
+ }
+ )
+
+ return () => {
+ const node = props.node
+ const renderIcon = (node: TreeNode) => {
+ const icon = node.designerProps.icon
+ if (icon) {
+ return
+ }
+ if (node === node.root) {
+ return
+ } else if (node.designerProps?.droppable) {
+ return
+ }
+ return
+ }
+
+ const parents = node.getParents()
+
+ const renderMenu = () => {
+ return (
+
+ {parents.slice(0, 4).map((parent) => {
+ return (
+
+ )
+ })}
+
+ )
+ }
+
+ return (
+
+
+ {expand.value && renderMenu()}
+
+ )
+ }
+ }
+ })
+)
+
+export const Selector = composeExport(SelectorComponent, {
+ displayName: 'Selector'
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/TranslateHandler.tsx b/packages/designable/src/widgets/AuxToolWidget/TranslateHandler.tsx
new file mode 100644
index 0000000..bdd7070
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/TranslateHandler.tsx
@@ -0,0 +1,37 @@
+import type { TreeNode } from '@pind/designable-core'
+import { defineComponent } from 'vue'
+import { useDesigner, usePrefix } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+
+export interface ITranslateHandlerProps {
+ node: TreeNode
+}
+
+export const TranslateHandler = defineComponent({
+ name: 'DnTranslateHandler',
+ props: ['node'],
+ setup(props) {
+ const designerRef = useDesigner()
+ const prefixRef = usePrefix('aux-node-translate-handler')
+
+ return () => {
+ const allowTranslate = props.node.allowTranslate()
+ if (!allowTranslate) return null
+
+ const createHandlerProps = (value: string) => ({
+ [designerRef.value.props.nodeTranslateAttrName!]: value,
+ class: {
+ [prefixRef.value]: true,
+ [value]: true
+ }
+ })
+
+ const handlerProps = createHandlerProps('translate')
+ return (
+
+
+
+ )
+ }
+ }
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/index.tsx b/packages/designable/src/widgets/AuxToolWidget/index.tsx
new file mode 100644
index 0000000..b564e5e
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/index.tsx
@@ -0,0 +1,96 @@
+// import React, { useEffect, useRef } from 'react'
+import { CursorStatus, CursorType } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent, onBeforeUnmount, ref } from 'vue'
+import { useCursor, useDesigner, useOperation, usePrefix, useViewport } from '../../hooks'
+import { ResizeHandleType } from '../../simulators/ResponsiveSimulator/handle'
+import { Cover } from './Cover'
+import { DashedBox } from './DashedBox'
+import { FreeSelection } from './FreeSelection'
+import { Insertion } from './Insertion'
+import { Selection } from './Selection'
+import './styles.less'
+
+const setCursorState = (contentWindow: Window, state: string) => {
+ const valueRoot = document?.getElementsByTagName?.('html')?.[0]
+ const root = contentWindow?.document?.getElementsByTagName('html')?.[0]
+ if (root) {
+ root.style.cursor = state
+ }
+ if (valueRoot) {
+ valueRoot.style.cursor = state
+ }
+}
+
+const AuxToolWidgetComponent = defineComponent({
+ name: 'DnAuxToolWidget',
+ props: [],
+ setup() {
+ const engineRef = useDesigner()
+ const viewportRef = useViewport()
+ const operationRef = useOperation()
+ const cursorRef = useCursor()
+ const prefixRef = usePrefix('aux-tool')
+ const _ref = ref()
+
+ const engineSubs: any[] = []
+
+ // [engine, viewport]
+ const cb1 = engineRef.value.subscribeWith('viewport:scroll', () => {
+ if (viewportRef.value.isIframe && _ref.value) {
+ _ref.value.style.transform = `perspective(1px) translate3d(${-viewportRef.value
+ .scrollX}px,${-viewportRef.value.scrollY}px,0)`
+ }
+ })
+ // [engine, cursor, viewportDragon, viewport, operation]
+ const cb2 = engineRef.value.subscribeWith(['drag:move', 'drag:stop'], () => {
+ if (cursorRef.value.status !== CursorStatus.Dragging) {
+ setCursorState(viewportRef.value.contentWindow, 'default')
+ } else {
+ if (cursorRef.value.type === CursorType.Move) {
+ if (operationRef.value.getDragNodes().length) {
+ // todo: update cursor will trigger document layout rerender https://bugs.chromium.org/p/chromium/issues/detail?id=664066
+ // if (viewportDragon.closestDirection === ClosestPosition.Inner) {
+ // setCursorState(viewport.contentWindow, 'copy')
+ // } else {
+ setCursorState(viewportRef.value.contentWindow, 'move')
+ //}
+ }
+ } else {
+ if (cursorRef.value.type === ResizeHandleType.ResizeWidth) {
+ setCursorState(viewportRef.value.contentWindow, 'ew-resize')
+ } else if (cursorRef.value.type === ResizeHandleType.ResizeHeight) {
+ setCursorState(viewportRef.value.contentWindow, 'ns-resize')
+ } else if (cursorRef.value.type === ResizeHandleType.Resize) {
+ setCursorState(viewportRef.value.contentWindow, 'nwse-resize')
+ } else {
+ setCursorState(viewportRef.value.contentWindow, 'default')
+ }
+ }
+ }
+ })
+ engineSubs.push(cb1, cb2)
+
+ onBeforeUnmount(() => {
+ engineSubs.map((engineCb) => engineCb())
+ })
+
+ return () => {
+ if (!viewportRef.value) return null
+
+ return (
+
+
+
+
+
+
+
+ )
+ }
+ }
+})
+
+export const AuxToolWidget = composeExport(AuxToolWidgetComponent, {
+ displayName: 'AuxToolWidget'
+})
diff --git a/packages/designable/src/widgets/AuxToolWidget/styles.less b/packages/designable/src/widgets/AuxToolWidget/styles.less
new file mode 100644
index 0000000..11e7d1b
--- /dev/null
+++ b/packages/designable/src/widgets/AuxToolWidget/styles.less
@@ -0,0 +1,274 @@
+@import '../../variables.less';
+
+@keyframes dn-animate-slide-to-top {
+ from {
+ transform: translateY(-10%);
+ opacity: 0;
+ }
+
+ to {
+ transform: translateY(0);
+ opacity: 0.8;
+ }
+}
+
+.@{prefix-cls}-aux-tool {
+ transform: perspective(1px) translate3d(0, 0, 0);
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ z-index: 2;
+}
+
+.@{prefix-cls}-aux-button {
+ button {
+ font-size: 12px !important;
+ display: flex;
+ align-items: center;
+ padding: 0 3px;
+ height: 20px;
+ color: var(--dn-aux-selector-btn-color); //#fff
+ background: var(--dn-aux-selector-btn-bg-color); //#1890ff
+ border-color: var(--dn-aux-selector-btn-border-color); //#1890ff
+
+ &:hover,
+ &:focus {
+ color: var(--dn-aux-selector-btn-hover-color); //#fff
+ background: var(--dn-aux-selector-btn-hover-bg-color); //#40a9ff
+ border-color: var(--dn-aux-selector-btn-hover-border-color); //#40a9ff
+ }
+
+ &:active {
+ color: var(--dn-aux-selector-btn-active-color); //#fff
+ background: var(--dn-aux-selector-btn-active-bg-color); //#096dd9
+ border-color: var(--dn-aux-selector-btn-active-border-color); //#096dd9
+ }
+ }
+}
+
+.@{prefix-cls}-aux-cover-rect {
+ &.dragging {
+ background-color: var(--dn-aux-cover-rect-dragging-color);
+ }
+
+ &.dropping {
+ background-color: var(--dn-aux-cover-rect-dropping-color);
+ }
+}
+
+.@{prefix-cls}-aux-free-selection {
+ background-color: var(--dn-aux-free-selection-background-color);
+ border-color: var(--dn-aux-free-selection-border-color);
+}
+
+.@{prefix-cls}-aux-helpers {
+ position: absolute;
+ pointer-events: all;
+ z-index: 10;
+ user-select: none;
+
+ .dn-aux-button();
+
+ &.bottom-right {
+ top: 100%;
+ right: 0;
+ }
+
+ &.bottom-left {
+ top: 100%;
+ left: 0;
+ }
+
+ &.bottom-center {
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ &.inner-top-right {
+ top: -2px;
+ right: 2px;
+ }
+
+ &.inner-top-left {
+ top: -2px;
+ left: 2px;
+ }
+
+ &.inner-top-center {
+ top: -2px;
+ right: 2px;
+ }
+
+ &.inner-bottom-right {
+ bottom: -2px;
+ right: 2px;
+ }
+
+ &.inner-bottom-left {
+ bottom: -2px;
+ left: 2px;
+ }
+
+ &.inner-bottom-center {
+ bottom: -2px;
+ right: 2px;
+ }
+
+ &.top-right {
+ bottom: 100%;
+ right: 0;
+ }
+
+ &.top-left {
+ bottom: 100%;
+ left: 0;
+ }
+
+ &.top-center {
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ button {
+ span {
+ transform: scale(0.9);
+ margin-left: 2px;
+
+ &.@{prefix-cls}-icon {
+ transform: scale(1);
+ margin-left: 0;
+ }
+ }
+ }
+
+ &-content {
+ display: flex;
+ flex-wrap: nowrap;
+ white-space: nowrap;
+
+ button {
+ font-size: 12px !important;
+ display: flex;
+ align-items: center;
+ padding: 0 3px;
+ height: 20px;
+ }
+
+ & > * {
+ margin-top: 4px;
+ margin-bottom: 4px;
+ margin-left: 2px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+ }
+}
+
+.@{prefix-cls}-aux-insertion {
+ background-color: var(--dn-aux-insertion-color);
+}
+
+.@{prefix-cls}-aux-dashed-box {
+ border: 1px dashed var(--dn-aux-dashed-box-color);
+
+ &-title {
+ color: var(--dn-aux-dashed-box-title-color);
+ }
+}
+
+.@{prefix-cls}-aux-selection-box {
+ border: 2px solid var(--dn-aux-selection-box-border-color);
+ position: relative;
+ pointer-events: none;
+
+ &-inner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ &-title {
+ color: var(--dn-aux-selection-box-color);
+ }
+}
+
+.@{prefix-cls}-aux-selector {
+ .dn-aux-button();
+
+ &-menu {
+ margin-top: -4px;
+ animation: dn-animate-slide-to-top 0.2s;
+ opacity: 0.8;
+
+ button {
+ font-size: 12px !important;
+ display: flex;
+ align-items: center;
+ padding: 0 3px;
+ height: 20px;
+ margin-top: 2px;
+ }
+ }
+}
+
+.@{prefix-cls}-aux-drag-handler {
+ cursor: move !important;
+}
+
+.@{prefix-cls}-aux-node-resize-handler {
+ position: absolute;
+ width: 8px;
+ height: 8px;
+ pointer-events: all;
+ background-color: var(--dn-brand-color);
+
+ &.x-start {
+ left: 0;
+ top: 50%;
+ transform: translate(calc(-50% - 1px), -50%);
+ cursor: ew-resize;
+ }
+
+ &.x-end {
+ left: 100%;
+ top: 50%;
+ transform: translate(calc(-50% + 1px), -50%);
+ cursor: ew-resize;
+ }
+
+ &.y-start {
+ left: 50%;
+ top: 0;
+ transform: translate(-50%, calc(-50% - 1px));
+ cursor: ns-resize;
+ }
+
+ &.y-end {
+ left: 50%;
+ top: 100%;
+ transform: translate(-50%, calc(-50% + 1px));
+ cursor: ns-resize;
+ }
+}
+
+.@{prefix-cls}-aux-node-translate-handler {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ width: 40px;
+ height: 20px;
+ background-color: var(--dn-brand-color);
+ opacity: 0.5;
+ pointer-events: all;
+}
diff --git a/packages/designable/src/widgets/ComponentTreeWidget/index.tsx b/packages/designable/src/widgets/ComponentTreeWidget/index.tsx
new file mode 100644
index 0000000..4221989
--- /dev/null
+++ b/packages/designable/src/widgets/ComponentTreeWidget/index.tsx
@@ -0,0 +1,109 @@
+import type { TreeNode } from '@pind/designable-core'
+import { GlobalRegistry } from '@pind/designable-core'
+import { observer } from '@formily/reactive-vue'
+import { FragmentComponent as Fragment } from '@formily/vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import type { PropType } from 'vue'
+import { defineComponent, provide, ref, toRef } from 'vue'
+import { DesignerComponentsSymbol, TreeNodeSymbol } from '../../context'
+import { useComponents, useDesigner, usePrefix, useTree } from '../../hooks'
+import type { IDesignerComponents } from '../../types'
+import './styles.less'
+
+export interface IComponentTreeWidgetProps {
+ components: IDesignerComponents
+}
+
+export interface ITreeNodeWidgetProps {
+ node: TreeNode
+}
+
+export const TreeNodeWidgetComponent = defineComponent({
+ name: 'DnTreeNodeWidget',
+ props: {
+ node: Object as PropType
+ },
+ setup(props) {
+ const designerRef = useDesigner(props.node?.designerProps?.effects)
+ const componentsRef = useComponents()
+
+ provide(TreeNodeSymbol, toRef(props, 'node'))
+
+ return () => {
+ const node = props.node
+ const renderChildren = () => {
+ if (node?.designerProps?.selfRenderChildren) return []
+ return node?.children?.map((child) => {
+ return
+ })
+ }
+
+ // may need to change
+ const renderProps = (extendsProps: any = {}) => {
+ return {
+ ...node.designerProps?.defaultProps,
+ ...extendsProps,
+ ...node.props,
+ ...node.designerProps?.getComponentProps?.(node)
+ }
+ }
+
+ const renderComponent = () => {
+ const componentName = node.componentName
+ const Component = componentsRef.value?.[componentName]
+
+ const dataId = {}
+ if (Component) {
+ if (designerRef.value) {
+ dataId[designerRef.value?.props?.nodeIdAttrName] = node.id
+ }
+ const { style, ...attrs } = renderProps(dataId)
+ return (
+
+ {renderChildren()}
+
+ )
+ } else {
+ if (node?.children?.length) {
+ return {renderChildren()}
+ }
+ }
+ }
+ if (!node) return null
+ if (node.hidden) return null
+ return renderComponent()
+ }
+ }
+})
+
+export const TreeNodeWidget = observer(TreeNodeWidgetComponent)
+
+export const ComponentTreeWidgetComponent = observer(
+ defineComponent({
+ name: 'DnComponentTreeWidget',
+ props: { components: [Object] },
+ setup(props) {
+ const treeRef = useTree()
+ const prefixRef = usePrefix('component-tree')
+ const designerRef = useDesigner()
+ const dataId = {}
+
+ GlobalRegistry.registerDesignerBehaviors(props.components as IDesignerComponents)
+ provide(DesignerComponentsSymbol, ref(toRef(props, 'components')))
+ if (designerRef.value && treeRef.value) {
+ dataId[designerRef.value?.props?.nodeIdAttrName] = treeRef.value.id
+ }
+ return () => {
+ return (
+
+
+
+ )
+ }
+ }
+ })
+)
+
+export const ComponentTreeWidget = composeExport(ComponentTreeWidgetComponent, {
+ displayName: 'ComponentTreeWidget'
+})
diff --git a/packages/designable/src/widgets/ComponentTreeWidget/styles.less b/packages/designable/src/widgets/ComponentTreeWidget/styles.less
new file mode 100644
index 0000000..7f5bfc9
--- /dev/null
+++ b/packages/designable/src/widgets/ComponentTreeWidget/styles.less
@@ -0,0 +1,7 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-component-tree {
+ position: relative;
+ min-height: 100%;
+ z-index: 1;
+}
diff --git a/packages/designable/src/widgets/DesignerToolsWidget/index.tsx b/packages/designable/src/widgets/DesignerToolsWidget/index.tsx
new file mode 100644
index 0000000..8da3bf2
--- /dev/null
+++ b/packages/designable/src/widgets/DesignerToolsWidget/index.tsx
@@ -0,0 +1,217 @@
+import { CursorType, ScreenType } from '@pind/designable-core'
+import { observer } from '@formily/reactive-vue'
+import { FragmentComponent as Fragment } from '@formily/vue'
+import { Button, InputNumber } from 'ant-design-vue'
+import type { PropType } from 'vue'
+import { defineComponent, reactive } from 'vue'
+import { useCursor, useHistory, usePrefix, useScreen, useWorkbench } from '../../hooks'
+import { useStyle } from '../../shared'
+import { IconWidget } from '../IconWidget'
+import './styles.less'
+
+type DesignerToolsType = 'HISTORY' | 'CURSOR' | 'SCREEN_TYPE'
+
+export type IDesignerToolsWidgetProps = {
+ use?: DesignerToolsType[]
+}
+
+const DesignerToolsWidgetComponent = defineComponent({
+ name: 'DnDesignerToolsWidget',
+ props: {
+ use: {
+ type: Array as PropType,
+ default: () => ['HISTORY', 'CURSOR', 'SCREEN_TYPE']
+ }
+ },
+ setup(props) {
+ const screenRef = useScreen()
+ const cursorRef = useCursor()
+ const workbenchRef = useWorkbench()
+ const historyRef = useHistory()
+ const sizeRef = reactive<{ width?: any; height?: any }>({
+ width: null,
+ height: null
+ })
+ const prefixRef = usePrefix('designer-tools')
+ const style = useStyle()
+
+ return () => {
+ const renderResponsiveController = () => {
+ if (!props.use.includes('SCREEN_TYPE')) return null
+ if (screenRef.value.type !== ScreenType.Responsive) return null
+ return (
+
+ {
+ sizeRef.width = value
+ }}
+ onPressEnter={() => {
+ screenRef.value.setSize(sizeRef.width, screenRef.value.height)
+ }}
+ />
+
+ {
+ sizeRef.height = value
+ }}
+ onPressEnter={() => {
+ screenRef.value.setSize(screenRef.value.width, sizeRef.height)
+ }}
+ />
+ {(screenRef.value.width !== '100%' || screenRef.value.height !== '100%') && (
+
+ )}
+
+ )
+ }
+
+ const renderScreenTypeController = () => {
+ if (!props.use.includes('SCREEN_TYPE')) return null
+ return (
+
+
+
+
+
+ )
+ }
+
+ const renderMobileController = () => {
+ if (!props.use.includes('SCREEN_TYPE')) return null
+ if (screenRef.value.type !== ScreenType.Mobile) return
+ return (
+
+ )
+ }
+
+ const renderHistoryController = () => {
+ if (!props.use.includes('HISTORY')) return null
+ return (
+
+
+
+
+ )
+ }
+ const renderCursorController = () => {
+ if (workbenchRef.value.type !== 'DESIGNABLE') return null
+ if (!props.use.includes('CURSOR')) return null
+ const cursor = cursorRef.value
+ return (
+
+
+
+
+ )
+ }
+ return (
+
+ {renderHistoryController()}
+ {renderCursorController()}
+ {renderScreenTypeController()}
+ {renderMobileController()}
+ {renderResponsiveController()}
+
+ )
+ }
+ }
+})
+export const DesignerToolsWidget = observer(DesignerToolsWidgetComponent)
diff --git a/packages/designable/src/widgets/DesignerToolsWidget/styles.less b/packages/designable/src/widgets/DesignerToolsWidget/styles.less
new file mode 100644
index 0000000..5e8ac63
--- /dev/null
+++ b/packages/designable/src/widgets/DesignerToolsWidget/styles.less
@@ -0,0 +1,29 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-designer-tools {
+ display: flex;
+ align-items: center;
+
+ .@{ant-prefix}-input-number {
+ font-size: 12px !important;
+ background: var(--dn-toolbar-input-bg-color) !important;
+ border-color: var(--dn-toolbar-input-border-color) !important;
+ color: var(--dn-toolbar-input-color) !important;
+
+ .@{ant-prefix}-input-number-handler-wrap,
+ .@{ant-prefix}-input-number-handler {
+ background: var(--dn-toolbar-input-handler-bg-color) !important;
+ border-color: var(--dn-toolbar-input-border-color) !important;
+ color: var(--dn-toolbar-input-color) !important;
+ }
+
+ .@{ant-prefix}-input-number-handler-down-inner,
+ .@{ant-prefix}-input-number-handler-up-inner {
+ color: var(--dn-toolbar-input-color) !important;
+ }
+
+ &:hover {
+ border-color: var(--dn-toolbar-input-hover-border-color) !important;
+ }
+ }
+}
diff --git a/packages/designable/src/widgets/DroppableWidget/index.tsx b/packages/designable/src/widgets/DroppableWidget/index.tsx
new file mode 100644
index 0000000..a7a3644
--- /dev/null
+++ b/packages/designable/src/widgets/DroppableWidget/index.tsx
@@ -0,0 +1,73 @@
+import type { TreeNode } from '@pind/designable-core'
+import { isStr } from '@pind/designable-shared'
+import { observer } from '@formily/reactive-vue'
+import type { PropType } from 'vue'
+import { defineComponent } from 'vue'
+import { useNodeIdProps, useTreeNode } from '../../hooks'
+import type { INodeActionsWidgetActionProps } from '../NodeActionsWidget'
+import { NodeActionsWidget } from '../NodeActionsWidget'
+import { NodeTitleWidget } from '../NodeTitleWidget'
+import './styles.less'
+
+export interface IDroppableWidgetProps {
+ node?: TreeNode
+ actions?: INodeActionsWidgetActionProps[]
+ height?: number
+ placeholder?: boolean
+ hasChildren?: boolean
+}
+
+export const DroppableWidget = observer(
+ defineComponent({
+ name: 'DnDroppableWidget',
+ inheritAttrs: false,
+ props: {
+ node: { type: Object as PropType },
+ height: {},
+ actions: {
+ type: Array as PropType>
+ },
+ placeholder: { type: Boolean as PropType, default: true },
+ hasChildren: { type: Boolean as PropType, default: undefined }
+ },
+ setup(props, { slots }) {
+ const nodeRef = useTreeNode()
+ const nodeIdRef = useNodeIdProps(props.node)
+
+ return () => {
+ const target = props.node ?? nodeRef.value
+ if (!target) return
+ const children = slots.default?.()
+ const hasChildren = props.hasChildren ?? (target.children?.length > 0 && children)
+ return (
+
+ {hasChildren ? (
+ children
+ ) : props.placeholder ? (
+
+
+
+ ) : (
+ children
+ )}
+ {props.actions?.length ? (
+
+ {props.actions.map((action, key) => (
+
+ ))}
+
+ ) : null}
+
+ )
+ }
+ }
+ })
+ // ({ node, actions, height, style, className, ...props }) => {
+
+ // }
+)
diff --git a/packages/designable/src/widgets/DroppableWidget/styles.less b/packages/designable/src/widgets/DroppableWidget/styles.less
new file mode 100644
index 0000000..d03f16a
--- /dev/null
+++ b/packages/designable/src/widgets/DroppableWidget/styles.less
@@ -0,0 +1,11 @@
+.dn-droppable-placeholder {
+ height: 60px;
+ background-color: var(--dn-droppable-bg-color);
+ border: 1px dashed var(--dn-droppable-border-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--dn-droppable-color);
+ font-weight: lighter;
+ font-size: 13px;
+}
diff --git a/packages/designable/src/widgets/EmptyWidget/index.tsx b/packages/designable/src/widgets/EmptyWidget/index.tsx
new file mode 100644
index 0000000..2c70566
--- /dev/null
+++ b/packages/designable/src/widgets/EmptyWidget/index.tsx
@@ -0,0 +1,58 @@
+import { defineComponent } from 'vue'
+import { observer } from '@formily/reactive-vue'
+import { useTree, usePrefix } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+import './styles.less'
+
+export interface IEmptyWidgetProps {
+ dragTipsDirection?: 'left' | 'right'
+}
+
+const EmptyWidgetComponent = defineComponent({
+ name: 'DnEmptyWidget',
+ props: {
+ dragTipsDirection: { type: String, default: 'left' }
+ },
+ setup(props, { slots }) {
+ const treeRef = useTree()
+ const prefixRef = usePrefix('empty')
+
+ return () => {
+ const renderEmpty = () => {
+ return (
+
+
+
+
+
+
+
+ Selection + Click / +
+ Click / + A
+
+
+ Copy + C / Paste + V
+
+
+ Delete
+
+
+
+ )
+ }
+
+ if (!treeRef.value?.children?.length) {
+ return {slots.default ? slots.default() : renderEmpty()}
+ }
+ return null
+ }
+ }
+})
+export const EmptyWidget = observer(EmptyWidgetComponent)
diff --git a/packages/designable/src/widgets/EmptyWidget/styles.less b/packages/designable/src/widgets/EmptyWidget/styles.less
new file mode 100644
index 0000000..e37d3cf
--- /dev/null
+++ b/packages/designable/src/widgets/EmptyWidget/styles.less
@@ -0,0 +1,27 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-empty {
+ transform: perspective(1px) translate3d(0, 0, 0);
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 100%;
+ width: 100%;
+ pointer-events: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--dn-empty-bg-color);
+ .animations {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+ .hotkeys-list {
+ text-align: center;
+ line-height: 30px;
+ color: #888;
+ }
+}
diff --git a/packages/designable/src/widgets/GhostWidget/index.tsx b/packages/designable/src/widgets/GhostWidget/index.tsx
new file mode 100644
index 0000000..75a5add
--- /dev/null
+++ b/packages/designable/src/widgets/GhostWidget/index.tsx
@@ -0,0 +1,73 @@
+import { defineComponent, ref, unref, onBeforeUnmount } from 'vue'
+import { CursorStatus } from '@pind/designable-core'
+import { autorun, observe } from '@formily/reactive'
+import { observer } from '@formily/reactive-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { useCursor, usePrefix, useDesigner } from '../../hooks'
+import { NodeTitleWidget } from '../NodeTitleWidget'
+import './styles.less'
+
+const GhostWidgetComponent = defineComponent({
+ name: 'DnGhostWidget',
+ setup() {
+ const designerRef = useDesigner()
+ const cursorRef = useCursor()
+ const refInstance = ref()
+ const prefixRef = usePrefix('ghost')
+
+ observe(cursorRef.value, () => {
+ const cursor = unref(cursorRef)
+ if (cursor.status === CursorStatus.Dragging) {
+ refInstance.value = document.querySelector('#GhostWidget')
+ } else {
+ refInstance.value = null
+ }
+ })
+
+ // [designer, cursor]
+ const dispose = autorun(() => {
+ const cursor = unref(cursorRef)
+ const ref = refInstance
+ const transform = `perspective(1px) translate3d(${cursor.position?.topClientX - 18}px,${
+ cursor.position?.topClientY - 12
+ }px,0) scale(0.8)`
+ if (!ref.value) return
+ ref.value.style.transform = transform
+ })
+
+ onBeforeUnmount(() => {
+ dispose && dispose()
+ })
+
+ return () => {
+ const designer = unref(designerRef)
+ const cursor = unref(cursorRef)
+
+ const draggingNodes = designer.findMovingNodes()
+ const firstNode = draggingNodes[0]
+
+ const renderNodes = () => {
+ return (
+
+
+ {draggingNodes.length > 1 ? '...' : ''}
+
+ )
+ }
+ if (!firstNode) return null
+ return cursor.status === CursorStatus.Dragging ? (
+
+ {renderNodes()}
+
+ ) : null
+ }
+ }
+})
+
+export const GhostWidget = composeExport(observer(GhostWidgetComponent), {
+ displayName: 'GhostWidget'
+})
diff --git a/packages/designable/src/widgets/GhostWidget/styles.less b/packages/designable/src/widgets/GhostWidget/styles.less
new file mode 100644
index 0000000..8fa6d54
--- /dev/null
+++ b/packages/designable/src/widgets/GhostWidget/styles.less
@@ -0,0 +1,20 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-ghost {
+ padding-left: 25px;
+ padding-right: 15px;
+ height: 30px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: fixed;
+ color: var(--dn-ghost-color);
+ font-size: 12px;
+ z-index: 9999;
+ border-radius: 50px;
+ background-color: var(--dn-ghost-bg-color);
+ pointer-events: none;
+ left: 0;
+ top: 0;
+ transform: translate3d(0, 0, 0);
+}
diff --git a/packages/designable/src/widgets/HistoryWidget/index.tsx b/packages/designable/src/widgets/HistoryWidget/index.tsx
new file mode 100644
index 0000000..6b42381
--- /dev/null
+++ b/packages/designable/src/widgets/HistoryWidget/index.tsx
@@ -0,0 +1,53 @@
+import format from 'dateformat'
+import { defineComponent } from 'vue'
+import { observer } from '@formily/reactive-vue'
+import { usePrefix, useWorkbench } from '../../hooks'
+import { TextWidget } from '../TextWidget'
+import './styles.less'
+
+export const HistoryWidget = observer(
+ defineComponent({
+ name: 'DnHistory',
+ props: [],
+ setup() {
+ const workbenchRef = useWorkbench()
+ const prefixRef = usePrefix('history')
+
+ return () => {
+ const currentWorkspace =
+ workbenchRef.value?.activeWorkspace || workbenchRef.value?.currentWorkspace
+ if (!currentWorkspace) return null
+ return (
+
+ {currentWorkspace.history.list().map((item, index) => {
+ const type = item.type || 'default_state'
+ const token = type.replace(/\:/g, '_')
+ return (
+
{
+ currentWorkspace.history.goTo(index)
+ }}
+ >
+
+
+
+
+ {' '}
+ {format(item.timestamp, 'yy/mm/dd HH:MM:ss')}
+
+
+ )
+ })}
+
+ )
+ }
+ }
+ })
+)
diff --git a/packages/designable/src/widgets/HistoryWidget/styles.less b/packages/designable/src/widgets/HistoryWidget/styles.less
new file mode 100644
index 0000000..c5b62c7
--- /dev/null
+++ b/packages/designable/src/widgets/HistoryWidget/styles.less
@@ -0,0 +1,23 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-history {
+ &-item {
+ display: flex;
+ justify-content: space-between;
+ min-height: 32px;
+ padding: 0 10px;
+ align-items: center;
+ cursor: pointer;
+ color: var(--dn-outline-tree-node-header-color);
+ &-timestamp {
+ font-size: 10px;
+ }
+ &:hover {
+ background-color: var(--dn-panel-active-bg-color);
+ }
+
+ &.active {
+ background-color: var(--dn-panel-active-bg-color);
+ }
+ }
+}
diff --git a/packages/designable/src/widgets/IconWidget/index.tsx b/packages/designable/src/widgets/IconWidget/index.tsx
new file mode 100644
index 0000000..c26882a
--- /dev/null
+++ b/packages/designable/src/widgets/IconWidget/index.tsx
@@ -0,0 +1,193 @@
+import { observer } from '@formily/reactive-vue'
+import { isObj, isPlainObj, isStr } from '@pind/designable-shared'
+import { composeExport, createContext, useContext } from '@formily/antdv/esm/__builtins__'
+import type { TooltipProps } from 'ant-design-vue'
+import { Tooltip } from 'ant-design-vue'
+import type { PropType, VNode } from 'vue'
+import { defineComponent, onBeforeUnmount, onMounted, ref, unref } from 'vue'
+import { usePrefix, useRegistry, useTheme } from '../../hooks'
+import { cloneElement, isVNode, useStyle } from '../../shared/util'
+import './styles.less'
+
+const IconContext = createContext(null)
+
+const isNumSize = (val: any) => /^[\d.]+$/.test(val)
+
+export interface IconProviderProps {
+ tooltip?: boolean
+}
+
+export interface IShadowSVGProps {
+ content?: string
+ width?: number | string
+ height?: number | string
+}
+
+export interface IIconWidgetProps extends HTMLElement {
+ tooltip?: Partial
+ infer: string | VNode | { shadow: string }
+ size?: number | string
+}
+
+const IconWidgetInner = observer(
+ defineComponent({
+ name: 'DnIconWidget',
+ emits: ['click'],
+ props: {
+ tooltip: {
+ type: [Object, String] as PropType
+ },
+ infer: {
+ type: [String, Function, Object] as PropType
+ },
+ size: { type: [Number, String] as PropType },
+ onClick: { type: Function }
+ },
+ setup(props, { attrs: _attrs, emit }) {
+ const themeRef = useTheme()
+ const IconContextRef = useContext(IconContext)
+ const registry = useRegistry()
+ const prefixRef = usePrefix('icon')
+
+ return () => {
+ const size = isNumSize(props.size) ? `${props.size}px` : props.size || '1em'
+ const attrs = _attrs
+ const style = useStyle()
+ const height = style?.height || size
+ const width = style?.width || size
+
+ const takeIcon = (infer: any) => {
+ const theme = unref(themeRef)
+ if (isStr(infer)) {
+ const fined = registry.getDesignerIcon(infer)
+ if (fined) {
+ return takeIcon(fined)
+ }
+ return
+ } else if (typeof infer?.render === 'function') {
+ const InferIcon = infer
+ return (
+
+ )
+ } else if (isVNode(infer)) {
+ if (infer.tag === 'svg') {
+ const Component = cloneElement(infer, {
+ height,
+ width,
+ fill: 'currentColor',
+ viewBox: infer.data?.attrs?.viewBox || '0 0 1024 1024',
+ focusable: 'false',
+ 'aria-hidden': 'true'
+ })
+ return Component
+ } else if (infer.tag === 'path' || infer.tag === 'g') {
+ return (
+
+ )
+ } else if (infer.componentOptions?.propsData?.content) {
+ // 判断是不是 shadowSVG === IconWidget.ShadowSVG 写死了看看后续怎么修改
+ return (
+
+ )
+ }
+ return infer
+ } else if (typeof infer === 'function') {
+ const InferIcon = infer
+ return
+ } else if (isPlainObj(infer)) {
+ if (infer[theme]) {
+ return takeIcon(infer[theme])
+ }
+ }
+ }
+
+ const renderTooltips = (children: any) => {
+ const IconContext = unref(IconContextRef)
+ if (!isStr(props.infer) && IconContext?.tooltip) return children
+ const tooltip = props.tooltip || registry.getDesignerMessage(`icons.${props.infer}`)
+ if (tooltip) {
+ const props = isObj(tooltip) ? tooltip : { title: tooltip }
+ return {children}
+ }
+ return children
+ }
+ return renderTooltips(
+ emit('click')}
+ >
+ {takeIcon(props.infer)}
+
+ )
+ }
+ }
+ })
+)
+
+const ShadowSVG = defineComponent({
+ props: {
+ width: [Number, String],
+ height: [Number, String],
+ content: String
+ },
+ setup(props: IShadowSVGProps) {
+ const refInstance = ref(null)
+ const width = isNumSize(props.width) ? `${props.width}px` : props.width
+ const height = isNumSize(props.height) ? `${props.height}px` : props.height
+
+ onMounted(() => {
+ if (refInstance.value) {
+ const root = refInstance.value.attachShadow({
+ mode: 'open'
+ })
+ root.innerHTML = ``
+ }
+ })
+
+ onBeforeUnmount(() => {
+ // TODO::报错
+ // if (!refInstance.value) return
+ // refInstance.value.attachShadow({
+ // mode: 'closed',
+ // })
+ })
+
+ return () =>
+ }
+})
+
+const Provider = defineComponent({
+ props: { tooltip: Boolean },
+ setup(props: IconProviderProps, { slots }) {
+ return () => {slots.default?.()}
+ }
+})
+
+export const IconWidget = composeExport(IconWidgetInner, {
+ ShadowSVG,
+ Provider
+})
diff --git a/packages/designable/src/widgets/IconWidget/styles.less b/packages/designable/src/widgets/IconWidget/styles.less
new file mode 100644
index 0000000..e0e55c9
--- /dev/null
+++ b/packages/designable/src/widgets/IconWidget/styles.less
@@ -0,0 +1,17 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-icon {
+ display: inline-block;
+ color: inherit;
+ font-style: normal;
+ line-height: 0;
+ text-align: center;
+ text-transform: none;
+ vertical-align: -0.125em;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ svg {
+ pointer-events: none;
+ }
+}
diff --git a/packages/designable/src/widgets/NodeActionsWidget/index.tsx b/packages/designable/src/widgets/NodeActionsWidget/index.tsx
new file mode 100644
index 0000000..dc589d6
--- /dev/null
+++ b/packages/designable/src/widgets/NodeActionsWidget/index.tsx
@@ -0,0 +1,76 @@
+import { observer } from '@formily/reactive-vue'
+import { Space } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { Button } from 'ant-design-vue'
+import type { VNode } from 'vue'
+import { defineComponent } from 'vue'
+import { usePrefix, useSelected, useTreeNode } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+import { TextWidget } from '../TextWidget'
+import './styles.less'
+
+export interface INodeActionsWidgetProps {
+ activeShown?: boolean
+}
+
+export interface INodeActionsWidgetActionProps {
+ title: VNode
+ icon?: VNode
+}
+
+const NodeActionsWidgetComponent = observer(
+ defineComponent({
+ name: 'DnNodeActionsWidget',
+ props: ['activeShown'],
+ setup(props, { slots }) {
+ const nodeRef = useTreeNode()
+ const prefixRef = usePrefix('node-actions')
+ const selectedRef = useSelected()
+ return () => {
+ if (selectedRef.value.indexOf(nodeRef.value.id) === -1 && props.activeShown) return null
+ return (
+
+
+
+ {slots.default?.()}
+
+
+
+ )
+ }
+ }
+ })
+)
+
+const ActionComponent = defineComponent({
+ name: 'DnAction',
+ props: ['icon', 'title', 'onClick'],
+ emits: ['click'],
+ setup(props, { attrs, emit }) {
+ const prefixRef = usePrefix('node-actions-item')
+ return () => {
+ return (
+
+ )
+ }
+ }
+})
+
+export const NodeActionsWidget = composeExport(NodeActionsWidgetComponent, {
+ Action: ActionComponent
+})
diff --git a/packages/designable/src/widgets/NodeActionsWidget/styles.less b/packages/designable/src/widgets/NodeActionsWidget/styles.less
new file mode 100644
index 0000000..d420595
--- /dev/null
+++ b/packages/designable/src/widgets/NodeActionsWidget/styles.less
@@ -0,0 +1,61 @@
+@import '../../variables.less';
+
+.dn-node-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ overflow: hidden;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ &-content {
+ position: relative;
+ padding: 0 20px;
+ display: flex;
+ align-items: center;
+ line-height: 1;
+ &::before {
+ position: absolute;
+ content: '';
+ display: block;
+ height: 0;
+ width: 300%;
+ top: 50%;
+ border-bottom: 1px dashed @border-color-split;
+ right: 100%;
+ }
+ &::after {
+ position: absolute;
+ content: '';
+ display: block;
+ height: 0;
+ width: 300%;
+ top: 50%;
+ border-bottom: 1px dashed @border-color-split;
+ left: 100%;
+ }
+ // a {
+ // color: @text-color-secondary;
+ // &:hover {
+ // color: @primary-color;
+ // }
+ // }
+ button {
+ color: @text-color-secondary;
+ &:hover {
+ color: @primary-color;
+ }
+ }
+ }
+ &-item {
+ &-text {
+ font-size: 10px;
+ display: flex;
+ align-items: center;
+ line-height: 1;
+ .dn-icon {
+ margin-right: 6px;
+ }
+ }
+ }
+}
diff --git a/packages/designable/src/widgets/NodePathWidget/index.tsx b/packages/designable/src/widgets/NodePathWidget/index.tsx
new file mode 100644
index 0000000..b3770d5
--- /dev/null
+++ b/packages/designable/src/widgets/NodePathWidget/index.tsx
@@ -0,0 +1,58 @@
+import { defineComponent, toRef } from 'vue'
+import { Breadcrumb } from 'ant-design-vue'
+import { FragmentComponent as Fragment } from '@formily/vue'
+import { useSelectedNode, useSelection, usePrefix, useHover } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+import { NodeTitleWidget } from '../NodeTitleWidget'
+import './styles.less'
+
+export interface INodePathWidgetProps {
+ workspaceId?: string
+ maxItems?: number
+}
+
+export const NodePathWidget = defineComponent({
+ name: 'DnNodePathWidget',
+ props: ['workspaceId', 'maxItems'],
+ setup(props) {
+ const workspaceId = toRef(props, 'workspaceId')
+ const selectedRef = useSelectedNode(workspaceId)
+ const selectionRef = useSelection(workspaceId)
+ const hoverRef = useHover(workspaceId)
+ const prefixRef = usePrefix('node-path')
+
+ return () => {
+ if (!selectedRef.value) return
+ const maxItems = props.maxItems ?? 3
+ const nodes = selectedRef.value
+ .getParents()
+ .slice(0, maxItems - 1)
+ .reverse()
+ .concat(selectedRef.value)
+ return (
+
+ {nodes.map((node, key) => {
+ return (
+
+ {key === 0 && }
+ {
+ hoverRef.value.setHover(node)
+ }}
+ onClick={(e) => {
+ e.stopPropagation()
+ e.preventDefault()
+ selectionRef.value.select(node)
+ }}
+ >
+
+
+
+ )
+ })}
+
+ )
+ }
+ }
+})
diff --git a/packages/designable/src/widgets/NodePathWidget/styles.less b/packages/designable/src/widgets/NodePathWidget/styles.less
new file mode 100644
index 0000000..26f5249
--- /dev/null
+++ b/packages/designable/src/widgets/NodePathWidget/styles.less
@@ -0,0 +1,16 @@
+// @import '~antd/lib/style/themes/default.less';
+@border-color-split: #f0f0f0;
+.dn-node-path {
+ padding: 4px 10px !important;
+ border-bottom: 1px solid @border-color-split;
+ .dn-icon {
+ font-size: 11px;
+ }
+ // .@{ant-prefix}-breadcrumb-separator {
+ // margin: 0 4px !important;
+ // }
+
+ a {
+ font-size: 12px;
+ }
+}
diff --git a/packages/designable/src/widgets/NodeTitleWidget/index.tsx b/packages/designable/src/widgets/NodeTitleWidget/index.tsx
new file mode 100644
index 0000000..06bf4c9
--- /dev/null
+++ b/packages/designable/src/widgets/NodeTitleWidget/index.tsx
@@ -0,0 +1,28 @@
+import { defineComponent } from 'vue'
+import { observer } from '@formily/reactive-vue'
+import { FragmentComponent as Fragment } from '@formily/vue'
+
+import type { TreeNode } from '@pind/designable-core'
+
+export interface INodeTitleWidgetProps {
+ node: TreeNode
+}
+
+const NodeTitleWidgetComponent = defineComponent({
+ name: 'DnNodeTitleWidget',
+ props: ['node'],
+ setup(props) {
+ const takeNode = () => {
+ const node = props.node
+ if (node.componentName === '$$ResourceNode$$') {
+ return node.children[0]
+ }
+ return node
+ }
+ return () => {
+ const node = takeNode()
+ return {node.getMessage('title') || node.componentName}
+ }
+ }
+})
+export const NodeTitleWidget = observer(NodeTitleWidgetComponent)
diff --git a/packages/designable/src/widgets/OutlineWidget/Insertion.tsx b/packages/designable/src/widgets/OutlineWidget/Insertion.tsx
new file mode 100644
index 0000000..6a95dc4
--- /dev/null
+++ b/packages/designable/src/widgets/OutlineWidget/Insertion.tsx
@@ -0,0 +1,78 @@
+import { observer } from '@formily/reactive-vue'
+import { ClosestPosition } from '@pind/designable-core'
+import { isNum } from '@pind/designable-shared'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent, toRef } from 'vue'
+import { useMoveHelper, usePrefix } from '../../hooks'
+
+export interface IInsertionProps {
+ workspaceId?: string
+}
+
+const InsertionComponent = defineComponent({
+ name: 'DnInsertion',
+ props: ['workspaceId'],
+ setup(props) {
+ const workspaceId = toRef(props, 'workspaceId')
+
+ const moveHelperRef = useMoveHelper(workspaceId)
+ const prefixRef = usePrefix('outline-tree-insertion')
+ return () => {
+ const moveHelper = moveHelperRef.value
+ const prefix = prefixRef.value
+ const createInsertionStyle = () => {
+ const closestDirection = moveHelper.viewportClosestDirection
+ const closestRect = moveHelper.viewportClosestOffsetRect
+ const baseStyle: Record = {
+ position: 'absolute',
+ transform: 'perspective(1px) translate3d(0,0,0)',
+ top: 0,
+ left: 0
+ }
+ if (!closestRect) return baseStyle
+ if (
+ closestDirection === ClosestPosition.After ||
+ closestDirection === ClosestPosition.InnerAfter ||
+ closestDirection === ClosestPosition.Under ||
+ closestDirection === ClosestPosition.ForbidAfter ||
+ closestDirection === ClosestPosition.ForbidInnerAfter ||
+ closestDirection === ClosestPosition.ForbidUnder
+ ) {
+ baseStyle.width = closestRect.width
+ baseStyle.height = 2
+ baseStyle.transform = `perspective(1px) translate3d(${closestRect.x}px,${
+ closestRect.y + closestRect.height - 2
+ }px,0)`
+ } else if (
+ closestDirection === ClosestPosition.Before ||
+ closestDirection === ClosestPosition.InnerBefore ||
+ closestDirection === ClosestPosition.Upper ||
+ closestDirection === ClosestPosition.ForbidBefore ||
+ closestDirection === ClosestPosition.ForbidInnerBefore ||
+ closestDirection === ClosestPosition.ForbidUpper
+ ) {
+ baseStyle.width = closestRect.width
+ baseStyle.height = 2
+ baseStyle.transform = `perspective(1px) translate3d(${closestRect.x}px,${closestRect.y}px,0)`
+ }
+ if (closestDirection.includes('FORBID')) {
+ baseStyle.backgroundColor = 'red'
+ } else {
+ baseStyle.backgroundColor = ''
+ }
+ Object.keys(baseStyle).forEach((key) => {
+ const value = baseStyle[key]
+ isNum(value) && (baseStyle[key] = value + 'px')
+ })
+ return baseStyle
+ }
+ if (!moveHelper?.closestNode) return null
+
+ return
+ }
+ }
+})
+
+export const Insertion = composeExport(observer(InsertionComponent), {
+ displayName: 'Insertion'
+})
diff --git a/packages/designable/src/widgets/OutlineWidget/OutlineNode.tsx b/packages/designable/src/widgets/OutlineWidget/OutlineNode.tsx
new file mode 100644
index 0000000..33b80f8
--- /dev/null
+++ b/packages/designable/src/widgets/OutlineWidget/OutlineNode.tsx
@@ -0,0 +1,196 @@
+import { autorun } from '@formily/reactive'
+import { observer } from '@formily/reactive-vue'
+import type { TreeNode } from '@pind/designable-core'
+import { ClosestPosition, CursorStatus, DragMoveEvent } from '@pind/designable-core'
+import { isFn } from '@pind/designable-shared'
+import { defineComponent, onBeforeUnmount, onMounted, ref, toRef } from 'vue'
+import { useContext } from '../../context'
+import { useCursor, useDesigner, useMoveHelper, usePrefix, useSelection } from '../../hooks'
+import { useStyle } from '../../shared/util'
+import { IconWidget } from '../IconWidget'
+import { NodeTitleWidget } from '../NodeTitleWidget'
+import { NodeSymbol } from './context'
+import './styles.less'
+
+export interface IOutlineTreeNodeProps {
+ node: TreeNode
+ workspaceId?: string
+}
+
+export const OutlineTreeNode = observer(
+ defineComponent({
+ name: 'DnOutlineTreeNode',
+ props: ['workspaceId', 'node'],
+ setup(props) {
+ const style = useStyle()
+
+ const prefix = usePrefix('outline-tree-node')
+ const engine = useDesigner()
+ const refInstance = ref()
+ const ctx = useContext(NodeSymbol)
+ const request = ref(null)
+ const cursor = useCursor()
+ const workspaceId = toRef(props, 'workspaceId')
+
+ const selection = useSelection(workspaceId)
+ const moveHelper = useMoveHelper(workspaceId)
+
+ // [node, outlineDragon, cursor]
+ const unSub = []
+ onMounted(() => {
+ const ref = refInstance
+ const subCb = engine.value.subscribeTo(DragMoveEvent, () => {
+ const closestNodeId = moveHelper.value?.closestNode?.id
+ const closestDirection = moveHelper.value?.outlineClosestDirection
+ const id = props.node.id
+ if (!ref.value) return
+ if (closestNodeId === id && closestDirection === ClosestPosition.Inner) {
+ if (!ref.value.classList.contains('droppable')) {
+ ref.value.classList.add('droppable')
+ }
+ if (!ref.value.classList.contains('expanded')) {
+ if (request.value) {
+ clearTimeout(request.value)
+ request.value = null
+ }
+ request.value = setTimeout(() => {
+ ref.value.classList.add('expanded')
+ }, 600)
+ }
+ } else {
+ if (request.value) {
+ clearTimeout(request.value)
+ request.value = null
+ }
+ if (ref.value.classList.contains('droppable')) {
+ ref.value.classList.remove('droppable')
+ }
+ }
+ })
+ unSub.push(subCb)
+ //[node, selection]
+ const subCb2 = autorun(() => {
+ const selectedIds = selection.value?.selected || []
+ const id = props.node.id
+ if (!ref.value) return
+ if (selectedIds.includes(id)) {
+ if (!ref.value.classList.contains('selected')) {
+ ref.value.classList.add('selected')
+ }
+ } else {
+ if (ref.value.classList.contains('selected')) {
+ ref.value.classList.remove('selected')
+ }
+ }
+ if (cursor.value.status === CursorStatus.Dragging) {
+ if (ref.value.classList.contains('selected')) {
+ ref.value.classList.remove('selected')
+ }
+ }
+ })
+ unSub.push(subCb2)
+ })
+
+ onBeforeUnmount(() => {
+ unSub.forEach((cb) => cb())
+ })
+
+ return () => {
+ const ref = refInstance
+ const node = props.node
+
+ if (!node) return null
+ const renderIcon = (node: TreeNode) => {
+ const icon = node.designerProps.icon
+ if (icon) {
+ return
+ }
+ if (node === node?.root) {
+ return
+ } else if (node.designerProps?.droppable) {
+ return
+ }
+ return
+ }
+
+ const renderTitle = (node: TreeNode) => {
+ if (isFn(ctx.value.renderTitle)) return ctx.value.renderTitle(node)
+ return (
+
+
+
+ )
+ }
+ const renderActions = (node: TreeNode) => {
+ if (isFn(ctx.value.renderActions)) return ctx.value.renderActions(node)
+ }
+ return (
+
+
+
+
+
+ {(node?.children?.length > 0 || node === node.root) && (
+
{
+ e.preventDefault()
+ e.stopPropagation()
+ if (ref.value?.classList?.contains('expanded')) {
+ ref.value?.classList.remove('expanded')
+ } else {
+ ref.value?.classList.add('expanded')
+ }
+ }}
+ >
+
+
+ )}
+
{renderIcon(node)}
+
{renderTitle(node)}
+
+
+ {renderActions(node)}
+ {node !== node.root && (
+ {
+ node.hidden = !node.hidden
+ }}
+ />
+ )}
+
+
+
+
+ {node.children?.map((child) => {
+ return (
+
+ )
+ })}
+
+
+ )
+ }
+ }
+ })
+)
diff --git a/packages/designable/src/widgets/OutlineWidget/context.ts b/packages/designable/src/widgets/OutlineWidget/context.ts
new file mode 100644
index 0000000..418946a
--- /dev/null
+++ b/packages/designable/src/widgets/OutlineWidget/context.ts
@@ -0,0 +1,9 @@
+import type { TreeNode } from '@pind/designable-core'
+import type { InjectionKey, Ref } from 'vue'
+
+interface INodeContext {
+ renderTitle?: (node: TreeNode) => any
+ renderActions?: (node: TreeNode) => any
+}
+
+export const NodeSymbol: InjectionKey[> = Symbol('INodeContext')
diff --git a/packages/designable/src/widgets/OutlineWidget/index.tsx b/packages/designable/src/widgets/OutlineWidget/index.tsx
new file mode 100644
index 0000000..f095327
--- /dev/null
+++ b/packages/designable/src/widgets/OutlineWidget/index.tsx
@@ -0,0 +1,75 @@
+// import React, { useRef, useLayoutEffect } from 'react'
+import { observer } from '@formily/reactive-vue'
+import type { Viewport } from '@pind/designable-core'
+import { computed, defineComponent, onMounted, provide, ref, unref } from 'vue'
+import { useOutline, usePrefix, useTree, useWorkbench } from '../../hooks'
+import { useStyle } from '../../shared/util'
+import { Insertion } from './Insertion'
+import { OutlineTreeNode } from './OutlineNode'
+import { NodeSymbol } from './context'
+
+export const OutlineTreeWidget = observer(
+ defineComponent({
+ props: ['renderActions', 'renderTitle', 'onClose'],
+ setup(props) {
+ const refInstance = ref(null)
+ const prefixRef = usePrefix('outline-tree')
+ const workbenchRef = useWorkbench()
+ const workspaceId = computed(() => {
+ const current = workbenchRef.value?.activeWorkspace || workbenchRef.value?.currentWorkspace
+ return current?.id
+ })
+
+ const treeRef = useTree(workspaceId)
+ const outline = useOutline(workspaceId)
+ const outlineRef = ref()
+ const style = useStyle()
+
+ provide(
+ NodeSymbol,
+ ref({
+ renderActions: props.renderActions,
+ renderTitle: props.renderTitle
+ })
+ )
+
+ // [workspaceId, outline]
+ // TODO::响应式有bug
+ onMounted(() => {
+ const _outline = outline.value
+ if (!workspaceId.value) return
+ if (outlineRef.value && outlineRef.value !== _outline) {
+ outlineRef.value.onUnmount()
+ }
+ if (refInstance.value && outline) {
+ _outline.onMount(refInstance.value, window)
+ }
+ outlineRef.value = _outline
+ return () => {
+ _outline.onUnmount()
+ }
+ })
+
+ return () => {
+ const prefix = unref(prefixRef)
+ const tree = unref(treeRef)
+ if (!outline.value || !workspaceId.value) return null
+ return (
+
+ )
+ }
+ }
+ })
+)
diff --git a/packages/designable/src/widgets/OutlineWidget/styles.less b/packages/designable/src/widgets/OutlineWidget/styles.less
new file mode 100644
index 0000000..7c7265c
--- /dev/null
+++ b/packages/designable/src/widgets/OutlineWidget/styles.less
@@ -0,0 +1,191 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-outline-tree {
+ &-container {
+ position: relative;
+ min-height: 100px;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ background-color: var(--dn-outline-tree-bg-color);
+ }
+
+ &-header {
+ display: flex;
+ padding: 8px;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--dn-outline-tree-header-border-color);
+ color: var(--dn-outline-tree-color);
+ }
+
+ &-title {
+ font-size: 16px;
+ font-weight: 500;
+ }
+
+ &-close {
+ display: flex;
+ align-items: center;
+ transform: scale(1.6);
+ cursor: pointer;
+ }
+
+ &-content {
+ position: relative;
+ flex-grow: 1;
+ height: 100%;
+ width: 100%;
+ padding-bottom: 20px;
+ overflow: overlay;
+ }
+
+ &-aux {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ &-insertion {
+ background-color: var(--dn-outline-tree-insertion-bg-color);
+ }
+
+ &-node {
+ position: relative;
+ user-select: none;
+ width: fit-content;
+ min-width: 100%;
+
+ &.expanded {
+ & > .@{prefix-cls}-outline-tree-node-header {
+ .@{prefix-cls}-outline-tree-node-expand {
+ transform: rotate(0);
+ }
+ }
+
+ & > .@{prefix-cls}-outline-tree-node-children {
+ display: block;
+ }
+ }
+
+ &.selected {
+ & > .@{prefix-cls}-outline-tree-node-header {
+ background-color: var(--dn-panel-active-bg-color);
+
+ .@{prefix-cls}-outline-tree-node-header-head {
+ background-color: var(--dn-panel-active-bg-color);
+ }
+ }
+ }
+
+ &.droppable {
+ & > .@{prefix-cls}-outline-tree-node-header {
+ .@{prefix-cls}-outline-tree-node-header-content {
+ .@{prefix-cls}-outline-tree-node-header-base {
+ & > .@{prefix-cls}-outline-tree-node-icon {
+ transform: scale(1.2);
+ }
+ }
+ }
+ }
+ }
+
+ &-hidden-icon:not(.hidden) {
+ display: none;
+ }
+
+ &-header {
+ display: flex;
+ min-height: 32px;
+ width: fit-content;
+ min-width: 100%;
+ align-items: center;
+ color: var(--dn-outline-tree-node-header-color);
+ position: relative;
+ padding-left: 8px;
+
+ &:hover {
+ .@{prefix-cls}-outline-tree-node-header-content {
+ color: var(--dn-outline-tree-node-hover-color);
+ }
+ }
+ }
+
+ &-header-head {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ pointer-events: none;
+ }
+
+ &-header-content {
+ display: flex;
+ align-items: center;
+ transition: all 0.15s ease-in;
+ transform-origin: left;
+ width: fit-content;
+ min-width: 100%;
+ height: 100%;
+ justify-content: space-between;
+ font-size: 12px;
+ }
+
+ &-header-base {
+ display: flex;
+ align-items: center;
+ }
+
+ &-header-actions {
+ display: flex;
+ align-items: center;
+ margin-right: 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &-expand {
+ display: flex;
+ align-items: center;
+ transition: all 0.15s ease-out;
+ transform: rotate(-90deg);
+ margin-right: 3px;
+ width: 12px;
+ }
+
+ &-icon {
+ margin-right: 5px;
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+
+ &-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-right: 100px;
+ }
+
+ &-actions {
+ font-size: 12px;
+ flex-grow: 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &-children {
+ padding-left: 16px;
+ display: none;
+ width: fit-content;
+ min-width: 100%;
+ }
+ }
+}
diff --git a/packages/designable/src/widgets/ResourceWidget/index.tsx b/packages/designable/src/widgets/ResourceWidget/index.tsx
new file mode 100644
index 0000000..38a003e
--- /dev/null
+++ b/packages/designable/src/widgets/ResourceWidget/index.tsx
@@ -0,0 +1,112 @@
+import type { IResource, IResourceLike } from '@pind/designable-core'
+import { isResourceHost, isResourceList } from '@pind/designable-core'
+import { isFn } from '@pind/designable-shared'
+import { observer } from '@formily/reactive-vue'
+import { defineComponent, ref, unref } from 'vue'
+import { usePrefix } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+import { TextWidget } from '../TextWidget'
+import './styles.less'
+
+export interface IResourceWidgetProps {
+ title: string
+ sources?: IResourceLike[]
+ defaultExpand?: boolean
+}
+
+export const ResourceWidget = observer(
+ defineComponent({
+ name: 'DnResourceWidget',
+ props: {
+ defaultExpand: { type: Boolean, default: true },
+ sources: { type: Array, default: () => [] },
+ title: String
+ },
+ setup(props, { slots }) {
+ const prefixRef = usePrefix('resource')
+ const expand = ref(props.defaultExpand)
+
+ const renderNode = (source: IResource) => {
+ const prefix = unref(prefixRef)
+ const { node, icon, title, thumb, span } = source
+ return (
+ ]
+ {thumb &&
}
+ {icon && (
+
+ )}
+
+ {title || node.children[0]?.getMessage('title')}
+
+
+ )
+ }
+
+ const sources = props.sources.reduce((buf, source) => {
+ if (isResourceList(source)) {
+ return buf.concat(source)
+ } else if (isResourceHost(source)) {
+ return buf.concat(source.Resource)
+ }
+ return buf
+ }, [])
+
+ const remainItems =
+ sources.reduce((length, source) => {
+ return length + (source.span ?? 1)
+ }, 0) % 3
+
+ return () => {
+ const prefix = unref(prefixRef)
+ return (
+
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ expand.value = !expand.value
+ }}
+ >
+
+
+
+
+ {props.title}
+
+
+
+
+ {sources.map(
+ isFn(slots.default) ? (slots.default as (source: IResource) => any) : renderNode
+ )}
+ {remainItems ? (
+
+ ) : null}
+
+
+
+ )
+ }
+ }
+ })
+)
diff --git a/packages/designable/src/widgets/ResourceWidget/styles.less b/packages/designable/src/widgets/ResourceWidget/styles.less
new file mode 100644
index 0000000..0c66ca6
--- /dev/null
+++ b/packages/designable/src/widgets/ResourceWidget/styles.less
@@ -0,0 +1,85 @@
+@import '../../variables.less';
+
+.@{prefix-cls}-resource {
+ flex-wrap: wrap;
+
+ &-header {
+ display: flex;
+ align-items: center;
+ padding: 5px 8px;
+ color: var(--dn-collapse-header-color);
+ border-bottom: 1px solid var(--dn-panel-border-color);
+ background-color: var(--dn-panel-active-bg-color);
+ cursor: pointer;
+ transition: all 0.25s ease-in-out;
+ font-size: 13px;
+
+ &-expand {
+ transform: rotate(-90deg);
+ font-size: 12px;
+ transition: all 0.15s ease-in-out;
+ margin-right: 3px;
+ }
+ }
+
+ &-content-wrapper {
+ display: flex;
+ justify-content: center;
+ background: var(--dn-resource-content-bg-color);
+ }
+
+ &-content {
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ display: none;
+ }
+
+ &.expand {
+ .@{prefix-cls}-resource-content {
+ display: grid;
+ grid-template-columns: repeat(3, 33.3333%);
+ grid-gap: 1px;
+ background-color: var(--dn-panel-border-color);
+ border-bottom: 1px solid var(--dn-panel-border-color);
+ }
+
+ .@{prefix-cls}-resource-header-expand {
+ transform: rotate(0);
+ }
+ }
+
+ &-item {
+ position: relative;
+ user-select: none;
+ background: var(--dn-resource-content-bg-color);
+ min-height: 40px;
+ color: var(--dn-resource-item-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ cursor: grab;
+ transition: color 0.1s ease-out;
+
+ &:hover {
+ color: var(--dn-resource-item-hover-color);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ z-index: 1;
+ }
+
+ &-icon {
+ margin: 12px 0;
+ }
+
+ &-text {
+ text-align: center;
+ font-size: 12px;
+ line-height: 1;
+ margin-bottom: 12px;
+ }
+ &-remain {
+ background: var(--dn-resource-content-bg-color);
+ }
+ }
+}
diff --git a/packages/designable/src/widgets/TextWidget/index.tsx b/packages/designable/src/widgets/TextWidget/index.tsx
new file mode 100644
index 0000000..87a3b3f
--- /dev/null
+++ b/packages/designable/src/widgets/TextWidget/index.tsx
@@ -0,0 +1,51 @@
+import { observer } from '@formily/reactive-vue'
+import type { IDesignerMiniLocales } from '@pind/designable-core'
+import { GlobalRegistry } from '@pind/designable-core'
+import { isPlainObj, isStr } from '@pind/designable-shared'
+import { defineComponent } from 'vue'
+
+const TextWidgetComponent = defineComponent({
+ name: 'DnTextWidget',
+ props: {
+ componentName: String,
+ sourceName: String,
+ token: String,
+ defaultMessage: String
+ },
+ setup(props, { slots }) {
+ const takeLocale = (message: string | IDesignerMiniLocales) => {
+ if (isStr(message)) return message
+ if (isPlainObj(message)) {
+ const lang = GlobalRegistry.getDesignerLanguage()
+ for (const key in message) {
+ if (key.toLocaleLowerCase() === lang) return message[key]
+ }
+ return
+ }
+ return message
+ }
+
+ const takeMessage = (token: any) => {
+ if (!token) return
+ const message = isStr(token) ? GlobalRegistry.getDesignerMessage(token) : token
+ if (message) return takeLocale(message)
+ return token
+ }
+ /**
+ * 子节点为TextNode的vnode
+ * 子节点为i18n对象
+ */
+ return () => {
+ return (
+ <>
+ {takeMessage(slots.default?.()?.[0]?.children) ||
+ takeMessage(slots.default?.()?.[0]) ||
+ takeMessage(props.token) ||
+ takeMessage(props.defaultMessage)}
+ >
+ )
+ }
+ }
+})
+
+export const TextWidget = observer(TextWidgetComponent)
diff --git a/packages/designable/src/widgets/ViewToolsWidget/index.tsx b/packages/designable/src/widgets/ViewToolsWidget/index.tsx
new file mode 100644
index 0000000..5e425d2
--- /dev/null
+++ b/packages/designable/src/widgets/ViewToolsWidget/index.tsx
@@ -0,0 +1,76 @@
+import type { WorkbenchTypes } from '@pind/designable-core'
+import { observer } from '@formily/reactive-vue'
+import { Button } from 'ant-design-vue'
+import type { PropType } from 'vue'
+import { defineComponent } from 'vue'
+import { usePrefix, useWorkbench } from '../../hooks'
+import { IconWidget } from '../IconWidget'
+
+export interface IViewToolsWidget {
+ use?: WorkbenchTypes[]
+}
+
+const VireToolsWidgetComponent = defineComponent({
+ name: 'DnVireToolsWidget',
+ props: {
+ use: {
+ type: Array as PropType,
+ default: () => ['DESIGNABLE', 'JSONTREE', 'PREVIEW']
+ }
+ },
+ setup(props) {
+ const workbenchRef = useWorkbench()
+ const prefixRef = usePrefix('view-tools')
+
+ return () => (
+
+ {props.use.includes('DESIGNABLE') && (
+
+ )}
+ {props.use.includes('JSONTREE') && (
+
+ )}
+ {props.use.includes('MARKUP') && (
+
+ )}
+ {props.use.includes('PREVIEW') && (
+
+ )}
+
+ )
+ }
+})
+
+export const ViewToolsWidget = observer(VireToolsWidgetComponent)
diff --git a/packages/designable/src/widgets/index.ts b/packages/designable/src/widgets/index.ts
new file mode 100644
index 0000000..8c44b1b
--- /dev/null
+++ b/packages/designable/src/widgets/index.ts
@@ -0,0 +1,15 @@
+export * from './AuxToolWidget'
+export * from './ComponentTreeWidget'
+export * from './DesignerToolsWidget'
+export * from './ViewToolsWidget'
+export * from './ResourceWidget'
+export * from './GhostWidget'
+export * from './EmptyWidget'
+export * from './OutlineWidget'
+export * from './IconWidget'
+export * from './TextWidget'
+export * from './HistoryWidget'
+export * from './NodePathWidget'
+export * from './NodeTitleWidget'
+export * from './DroppableWidget'
+export * from './NodeActionsWidget'
diff --git a/packages/renderer/tsconfig.build.json b/packages/designable/tsconfig.build.json
similarity index 65%
rename from packages/renderer/tsconfig.build.json
rename to packages/designable/tsconfig.build.json
index cb94a64..0d5dfcd 100644
--- a/packages/renderer/tsconfig.build.json
+++ b/packages/designable/tsconfig.build.json
@@ -1,10 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
+ "baseUrl": "./src",
"outDir": "./lib",
- "paths": {
- "@formily/*": ["packages/*"]
- },
"declaration": true
}
}
diff --git a/packages/renderer/tsconfig.json b/packages/designable/tsconfig.json
similarity index 100%
rename from packages/renderer/tsconfig.json
rename to packages/designable/tsconfig.json
diff --git a/packages/prototypes/babel.config.js b/packages/prototypes/babel.config.js
new file mode 100644
index 0000000..4b763bc
--- /dev/null
+++ b/packages/prototypes/babel.config.js
@@ -0,0 +1,15 @@
+module.exports = (api) => {
+ return {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ useBuiltIns: false,
+ modules: api.env(['es']) ? false : api.env(['cjs']) ? 'commonjs' : 'auto'
+ }
+ ],
+ '@babel/preset-typescript'
+ ],
+ plugins: ['@babel/transform-runtime', '@vue/babel-plugin-jsx']
+ }
+}
diff --git a/packages/prototypes/builder.config.ts b/packages/prototypes/builder.config.ts
index dc7bd55..6feaa4f 100644
--- a/packages/prototypes/builder.config.ts
+++ b/packages/prototypes/builder.config.ts
@@ -4,4 +4,10 @@ export const BuilderConfig: IBuilderConfig = {
targetLibName: 'ant-design-vue',
targetLibCjsDir: 'lib',
targetLibEsDir: 'es',
+ externals: {
+ '@formily/antdv': 'Formily.Antdv',
+ '@formily/antdv-designable': 'Formily.AntdvDesignable',
+ '@formily/antdv-setters': 'Formily.AntdvSetters',
+ '@formily/antdv-settings-form': 'Formily.AntdvSettingsForm'
+ }
}
diff --git a/packages/prototypes/package.json b/packages/prototypes/package.json
index 5467c66..0569483 100644
--- a/packages/prototypes/package.json
+++ b/packages/prototypes/package.json
@@ -1,7 +1,6 @@
{
"name": "@formily/antdv-prototypes",
- "private": true,
- "version": "1.0.0-alpha.8",
+ "version": "2.0.0",
"license": "MIT",
"main": "lib",
"types": "lib/index.d.ts",
@@ -23,7 +22,7 @@
"**/*/style.js"
],
"scripts": {
- "build": "formily-tpl build"
+ "build": "ts-node ../../scripts/build.ts"
},
"repository": {
"type": "git",
@@ -36,17 +35,24 @@
"publishConfig": {
"access": "public"
},
+ "maintainers": [
+ "yiyunwan"
+ ],
"peerDependencies": {
- "@vue/composition-api": "^1.0.0-beta.1",
- "vue": "^2.6.0 || >=3.0.0-rc.0"
- },
- "peerDependenciesMeta": {
- "@vue/composition-api": {
- "optional": true
- }
+ "vue": "^3.3.0"
},
"devDependencies": {
- "ant-design-vue": "^1.7.8",
- "vue": "^2.6.14"
- }
+ "@formily/core": "^2.0.0",
+ "@formily/json-schema": "^2.0.0",
+ "@formily/reactive": "^2.0.0",
+ "@formily/reactive-vue": "^2.0.0",
+ "@formily/shared": "^2.0.0",
+ "@formily/vue": "^2.0.0",
+ "@pind/designable-core": "^2.0.0-beta.6",
+ "@pind/designable-shared": "^2.0.0-beta.6",
+ "@formily/antdv": "2.0.0",
+ "@formily/antdv-designable": "2.0.0",
+ "@formily/antdv-setters": "2.0.0"
+ },
+ "gitHead": "62ef79d985a056c1038754fc928645c3b147cb6e"
}
diff --git a/packages/prototypes/src/common/Container/index.tsx b/packages/prototypes/src/common/Container/index.tsx
new file mode 100644
index 0000000..7229cf5
--- /dev/null
+++ b/packages/prototypes/src/common/Container/index.tsx
@@ -0,0 +1,27 @@
+import { uid } from '@pind/designable-shared'
+import { DroppableWidget } from '@formily/antdv-designable'
+import { defineComponent } from 'vue'
+import './styles.less'
+
+export const Container = defineComponent({
+ name: 'DnContainer',
+ setup(props, { slots }) {
+ return () => {
+ return {slots}
+ }
+ }
+})
+
+export const withContainer = (Target: any) => {
+ return defineComponent({
+ setup(props, { attrs, slots }) {
+ return () => {
+ return (
+
+ {slots}
+
+ )
+ }
+ }
+ })
+}
diff --git a/packages/prototypes/src/common/Container/styles.less b/packages/prototypes/src/common/Container/styles.less
new file mode 100644
index 0000000..2b8430b
--- /dev/null
+++ b/packages/prototypes/src/common/Container/styles.less
@@ -0,0 +1,7 @@
+@import '../../styles.less';
+
+.dn-form-container {
+ margin: 0 !important;
+ padding: 20px;
+ border: 1px solid @border-color-split;
+}
diff --git a/packages/prototypes/src/common/FormItemSwitcher/index.tsx b/packages/prototypes/src/common/FormItemSwitcher/index.tsx
new file mode 100644
index 0000000..45831a1
--- /dev/null
+++ b/packages/prototypes/src/common/FormItemSwitcher/index.tsx
@@ -0,0 +1,23 @@
+import { Switch } from '@formily/antdv'
+import { defineComponent } from 'vue'
+
+export interface IFormItemSwitcherProps {
+ value?: string
+}
+
+export const FormItemSwitcher = defineComponent({
+ props: { value: {} },
+ emits: ['change'],
+ setup(props, { emit }) {
+ return () => {
+ return (
+ {
+ emit('change', value ? 'FormItem' : undefined)
+ }}
+ />
+ )
+ }
+ }
+})
diff --git a/packages/prototypes/src/common/LoadTemplate/index.tsx b/packages/prototypes/src/common/LoadTemplate/index.tsx
new file mode 100644
index 0000000..d146dd6
--- /dev/null
+++ b/packages/prototypes/src/common/LoadTemplate/index.tsx
@@ -0,0 +1,32 @@
+import { NodeActionsWidget } from '@formily/antdv-designable'
+import type { VNode } from 'vue'
+import { defineComponent } from 'vue'
+
+export interface ITemplateAction {
+ title: VNode
+ tooltip?: VNode
+ icon?: string | VNode
+ onClick: () => void
+}
+
+export interface ILoadTemplateProps {
+ className?: string
+ style?: Record
+ actions?: ITemplateAction[]
+}
+
+export const LoadTemplate = defineComponent({
+ emits: ['click'],
+ props: { actions: Array },
+ setup(props) {
+ return () => {
+ return (
+
+ {props.actions?.map((action: ITemplateAction, key) => {
+ return
+ })}
+
+ )
+ }
+ }
+})
diff --git a/packages/prototypes/src/components/ArrayBase/index.ts b/packages/prototypes/src/components/ArrayBase/index.ts
new file mode 100644
index 0000000..ad2cbb7
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayBase/index.ts
@@ -0,0 +1,79 @@
+import { createBehavior } from '@pind/designable-core'
+import { createFieldSchema, createVoidFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const createArrayBehavior = (name: string) => {
+ return createBehavior(
+ {
+ name,
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === name,
+ designerProps: {
+ droppable: true,
+ propsSchema: createFieldSchema(AllSchemas[name])
+ },
+ designerLocales: AllLocales[name]
+ },
+ {
+ name: `${name}.Addition`,
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === `${name}.Addition`,
+ designerProps: {
+ allowDrop(parent) {
+ return parent.props['x-component'] === name
+ },
+ propsSchema: createVoidFieldSchema(AllSchemas[name].Addition)
+ },
+ designerLocales: AllLocales.ArrayAddition
+ },
+ {
+ name: `${name}.Remove`,
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === `${name}.Remove`,
+ designerProps: {
+ allowDrop(parent) {
+ return parent.props['x-component'] === name
+ },
+ propsSchema: createVoidFieldSchema()
+ },
+ designerLocales: AllLocales.ArrayRemove
+ },
+ {
+ name: `${name}.Index`,
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === `${name}.Index`,
+ designerProps: {
+ allowDrop(parent) {
+ return parent.props['x-component'] === name
+ },
+ propsSchema: createVoidFieldSchema()
+ },
+ designerLocales: AllLocales.ArrayIndex
+ },
+ {
+ name: `${name}.MoveUp`,
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === `${name}.MoveUp`,
+ designerProps: {
+ allowDrop(parent) {
+ return parent.props['x-component'] === name
+ },
+ propsSchema: createVoidFieldSchema()
+ },
+ designerLocales: AllLocales.ArrayMoveUp
+ },
+ {
+ name: `${name}.MoveDown`,
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === `${name}.MoveDown`,
+ designerProps: {
+ allowDrop(parent) {
+ return parent.props['x-component'] === 'ArrayCards'
+ },
+ propsSchema: createVoidFieldSchema()
+ },
+ designerLocales: AllLocales.ArrayMoveDown
+ }
+ )
+}
diff --git a/packages/prototypes/src/components/ArrayCards/index.ts b/packages/prototypes/src/components/ArrayCards/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayCards/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/ArrayCards/preview.tsx b/packages/prototypes/src/components/ArrayCards/preview.tsx
new file mode 100644
index 0000000..ac53732
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayCards/preview.tsx
@@ -0,0 +1,275 @@
+import { defineComponent } from 'vue'
+import { Card, Row } from 'ant-design-vue'
+import { TreeNode, createResource } from '@pind/designable-core'
+import { uid } from '@pind/designable-shared'
+import {
+ useTreeNode,
+ TreeNodeWidget,
+ DroppableWidget,
+ useNodeIdProps
+} from '@formily/antdv-designable'
+import { ArrayBase } from '@formily/antdv'
+import { observer } from '@formily/reactive-vue'
+import { composeExport, usePrefixCls } from '@formily/antdv/esm/__builtins__'
+import {
+ hasNodeByComponentPath,
+ queryNodesByComponentPath,
+ createEnsureTypeItemsNode,
+ findNodeByComponentPath,
+ createNodeId
+} from '../../shared'
+import { useDropTemplate } from '../../hooks'
+import { LoadTemplate } from '../../common/LoadTemplate'
+import { createArrayBehavior } from '../ArrayBase'
+import './styles.less'
+
+const ensureObjectItemsNode = createEnsureTypeItemsNode('object')
+
+const isArrayCardsOperation = (name: string) =>
+ name === 'ArrayCards.Remove' || name === 'ArrayCards.MoveDown' || name === 'ArrayCards.MoveUp'
+
+export const ArrayCards = composeExport(
+ observer(
+ defineComponent({
+ props: { title: {} },
+ setup(props, { attrs, slots }) {
+ const nodeRef = useTreeNode()
+ const nodeIdRef = useNodeIdProps()
+ const formilyArrayCardsPrefix = usePrefixCls('formily-array-cards')
+
+ const designerRef = useDropTemplate('ArrayCards', (source) => {
+ const indexNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayCards.Index'
+ }
+ })
+ const additionNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'void',
+ title: 'Addition',
+ 'x-component': 'ArrayCards.Addition'
+ }
+ })
+ const removeNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayCards.Remove'
+ }
+ })
+ const moveDownNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayCards.MoveDown'
+ }
+ })
+ const moveUpNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayCards.MoveUp'
+ }
+ })
+
+ const objectNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'object'
+ },
+ children: [indexNode, ...source, removeNode, moveDownNode, moveUpNode]
+ })
+ return [objectNode, additionNode]
+ })
+
+ return () => {
+ const node = nodeRef.value
+ const nodeId = nodeIdRef.value
+ const designer = designerRef.value
+
+ const renderCard = () => {
+ if (node.children.length === 0) return
+ const additions = queryNodesByComponentPath(node, ['ArrayCards', 'ArrayCards.Addition'])
+ const indexes = queryNodesByComponentPath(node, ['ArrayCards', '*', 'ArrayCards.Index'])
+ const operations = queryNodesByComponentPath(node, [
+ 'ArrayCards',
+ '*',
+ isArrayCardsOperation
+ ])
+ const children = queryNodesByComponentPath(node, [
+ 'ArrayCards',
+ '*',
+ (name) => name.indexOf('ArrayCards.') === -1
+ ])
+
+ return (
+
+
+
+ {indexes.map((node) => (
+
+ ))}
+ {props.title}
+
+ }
+ extra={
+
+
+ {operations.map((node) => (
+
+ ))}
+ {slots.extra?.()}
+
+
+ }
+ >
+
+ {children.length ? (
+ children.map((node) => )
+ ) : (
+
+ )}
+
+
+
+ {additions.map((node) => {
+ return
+ })}
+
+
+
+ )
+ }
+
+ return (
+
+ {renderCard()}
+ {
+ if (hasNodeByComponentPath(node, ['ArrayCards', '*', 'ArrayCards.Index']))
+ return
+ const indexNode = new TreeNode({
+ componentName: node.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'Button'
+ }
+ })
+ ensureObjectItemsNode(node).append(indexNode)
+ }
+ },
+ {
+ title: node.getMessage('addOperation'),
+ icon: 'AddOperation',
+ onClick: () => {
+ const oldAdditionNode = findNodeByComponentPath(node, [
+ 'ArrayCards',
+ 'ArrayCards.Addition'
+ ])
+ if (!oldAdditionNode) {
+ const additionNode = new TreeNode({
+ componentName: node.componentName,
+ props: {
+ type: 'void',
+ title: 'Addition',
+ 'x-component': 'ArrayCards.Addition'
+ }
+ })
+ ensureObjectItemsNode(node).insertAfter(additionNode)
+ }
+ const oldRemoveNode = findNodeByComponentPath(node, [
+ 'ArrayCards',
+ '*',
+ 'ArrayCards.Remove'
+ ])
+ const oldMoveDownNode = findNodeByComponentPath(node, [
+ 'ArrayCards',
+ '*',
+ 'ArrayCards.MoveDown'
+ ])
+ const oldMoveUpNode = findNodeByComponentPath(node, [
+ 'ArrayCards',
+ '*',
+ 'ArrayCards.MoveUp'
+ ])
+ if (!oldRemoveNode) {
+ ensureObjectItemsNode(node).append(
+ new TreeNode({
+ componentName: node.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayCards.Remove'
+ }
+ })
+ )
+ }
+ if (!oldMoveDownNode) {
+ ensureObjectItemsNode(node).append(
+ new TreeNode({
+ componentName: node.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayCards.MoveDown'
+ }
+ })
+ )
+ }
+ if (!oldMoveUpNode) {
+ ensureObjectItemsNode(node).append(
+ new TreeNode({
+ componentName: node.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayCards.MoveUp'
+ }
+ })
+ )
+ }
+ }
+ }
+ ]}
+ />
+
+ )
+ }
+ }
+ })
+ ),
+ {
+ Behavior: createArrayBehavior('ArrayCards'),
+ Resource: createResource({
+ icon: 'ArrayCardsSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'array',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'ArrayCards',
+ 'x-component-props': {
+ title: `Title`
+ }
+ }
+ }
+ ]
+ }),
+ Addition: ArrayBase.Addition,
+ Index: ArrayBase.Index,
+ Item: ArrayBase.Item,
+ MoveDown: ArrayBase.MoveDown,
+ MoveUp: ArrayBase.MoveUp,
+ Remove: ArrayBase.Remove,
+ SortHandle: ArrayBase.SortHandle
+ }
+)
diff --git a/packages/prototypes/src/components/ArrayCards/styles.less b/packages/prototypes/src/components/ArrayCards/styles.less
new file mode 100644
index 0000000..c468465
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayCards/styles.less
@@ -0,0 +1,5 @@
+@import '../../styles.less';
+
+.dn-array-cards {
+ background-color: @component-background;
+}
diff --git a/packages/prototypes/src/components/ArrayItems/index.ts b/packages/prototypes/src/components/ArrayItems/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayItems/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/ArrayItems/preview.tsx b/packages/prototypes/src/components/ArrayItems/preview.tsx
new file mode 100644
index 0000000..05a5e1d
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayItems/preview.tsx
@@ -0,0 +1,117 @@
+import { defineComponent } from 'vue'
+import { Row } from 'ant-design-vue'
+import { TreeNode, createResource } from '@pind/designable-core'
+import {
+ useTreeNode,
+ TreeNodeWidget,
+ DroppableWidget,
+ useNodeIdProps
+} from '@formily/antdv-designable'
+import { ArrayBase } from '@formily/antdv'
+import { observer } from '@formily/reactive-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { uid } from '@pind/designable-shared'
+import { queryNodesByComponentPath, createEnsureTypeItemsNode, createNodeId } from '../../shared'
+import { useDropTemplate } from '../../hooks'
+import { createArrayBehavior } from '../ArrayBase'
+import './styles.less'
+
+const ensureObjectItemsNode = createEnsureTypeItemsNode('object')
+
+export const ArrayItems = composeExport(
+ observer(
+ defineComponent({
+ props: [],
+ setup() {
+ const nodeRef = useTreeNode()
+ const nodeIdRef = useNodeIdProps()
+
+ const designerRef = useDropTemplate('ArrayItems', (source) => {
+ const additionNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'void',
+ title: 'Addition',
+ 'x-component': 'ArrayItems.Addition'
+ }
+ })
+ const removeNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayItems.Remove'
+ }
+ })
+ const objectNode = new TreeNode({
+ componentName: nodeRef.value.componentName,
+ props: {
+ type: 'object'
+ },
+ children: [...source, removeNode]
+ })
+ return [objectNode, additionNode]
+ })
+
+ return () => {
+ const node = nodeRef.value
+ const nodeId = nodeIdRef.value
+ const designer = designerRef.value
+
+ const renderItems = () => {
+ if (node.children.length === 0) return
+ const additions = queryNodesByComponentPath(node, ['ArrayItems', 'ArrayItems.Addition'])
+ const children = queryNodesByComponentPath(node, [
+ 'ArrayItems',
+ '*',
+ (name) => name.indexOf('ArrayItems.') === -1
+ ])
+
+ return (
+
+
+
+ {children.length ? (
+ children.map((node) => )
+ ) : (
+
+ )}
+
+
+ {additions.map((node) => {
+ return
+ })}
+
+
+
+ )
+ }
+
+ return (
+
+ {renderItems()}
+
+ )
+ }
+ }
+ })
+ ),
+ {
+ Behavior: createArrayBehavior('ArrayItems'),
+ Resource: createResource({
+ icon: 'ArrayCardsSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'array',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'ArrayItems'
+ }
+ }
+ ]
+ }),
+ Addition: ArrayBase.Addition,
+ Item: ArrayBase.Item,
+ Remove: ArrayBase.Remove
+ }
+)
diff --git a/packages/prototypes/src/components/ArrayItems/styles.less b/packages/prototypes/src/components/ArrayItems/styles.less
new file mode 100644
index 0000000..0d9cc3c
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayItems/styles.less
@@ -0,0 +1,5 @@
+@import '../../styles.less';
+
+.dn-array-items {
+ background-color: @component-background;
+}
diff --git a/packages/prototypes/src/components/ArrayTable/index.ts b/packages/prototypes/src/components/ArrayTable/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayTable/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/ArrayTable/preview.tsx b/packages/prototypes/src/components/ArrayTable/preview.tsx
new file mode 100644
index 0000000..e05677b
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayTable/preview.tsx
@@ -0,0 +1,454 @@
+import { defineComponent } from 'vue'
+import { Table, Row } from 'ant-design-vue'
+import { observer } from '@formily/reactive-vue'
+import { TreeNode, createBehavior, createResource } from '@pind/designable-core'
+import { uid } from '@pind/designable-shared'
+import {
+ useDesigner,
+ useTreeNode,
+ TreeNodeWidget,
+ DroppableWidget,
+ useNodeIdProps
+} from '@formily/antdv-designable'
+import { ArrayBase } from '@formily/antdv'
+import { composeExport, usePrefixCls } from '@formily/antdv/esm/__builtins__'
+import { AllLocales } from '../../locales'
+import { AllSchemas } from '../../schemas'
+import { useDropTemplate } from '../../hooks'
+import {
+ queryNodesByComponentPath,
+ hasNodeByComponentPath,
+ findNodeByComponentPath,
+ createEnsureTypeItemsNode
+} from '../../shared'
+import { LoadTemplate } from '../../common/LoadTemplate'
+import { createVoidFieldSchema } from '../Field'
+import { createArrayBehavior } from '../ArrayBase'
+import './styles.less'
+
+const ensureObjectItemsNode = createEnsureTypeItemsNode('object')
+
+// TableProps
+export const ArrayTable = composeExport(
+ observer(
+ defineComponent({
+ props: [],
+ setup(props, { attrs }) {
+ const designerRef = useDesigner()
+ const nodeRef = useTreeNode()
+ const nodeIdRef = useNodeIdProps()
+ const formilyArrayTablePrefix = usePrefixCls('formily-array-table')
+
+ useDropTemplate('ArrayTable', (source) => {
+ const sortHandleNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `Title`
+ }
+ },
+ children: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.SortHandle'
+ }
+ }
+ ]
+ })
+ const indexNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `Title`
+ }
+ },
+ children: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Index'
+ }
+ }
+ ]
+ })
+ const columnNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `Title`
+ }
+ },
+ children: source.map((node) => {
+ node.props.title = undefined
+ return node
+ })
+ })
+
+ const operationNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `Title`
+ }
+ },
+ children: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Remove'
+ }
+ },
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.MoveDown'
+ }
+ },
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.MoveUp'
+ }
+ }
+ ]
+ })
+ const objectNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'object'
+ },
+ children: [sortHandleNode, indexNode, columnNode, operationNode]
+ })
+ const additionNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ title: `Addition`,
+ 'x-component': 'ArrayTable.Addition'
+ }
+ })
+ return [objectNode, additionNode]
+ })
+
+ useDropTemplate('ArrayTable.Column', (source) => {
+ return source.map((node) => {
+ node.props.title = undefined
+ return node
+ })
+ })
+
+ return () => {
+ const node = nodeRef.value
+ const nodeId = nodeIdRef.value
+ const columns = queryNodesByComponentPath(node, ['ArrayTable', '*', 'ArrayTable.Column'])
+ const additions = queryNodesByComponentPath(node, ['ArrayTable', 'ArrayTable.Addition'])
+
+ const defaultRowKey = () => {
+ return node.id
+ }
+
+ const renderTable = () => {
+ if (node.children.length === 0) return
+ return (
+
+ {/* TODO:: rerender table cuz table resizes when insert new value */}
+
+
+ {additions.map((node) => {
+ return
+ })}
+
+
+ )
+ }
+ return (
+
+ {renderTable()}
+ {
+ if (
+ hasNodeByComponentPath(node, [
+ 'ArrayTable',
+ '*',
+ 'ArrayTable.Column',
+ 'ArrayTable.SortHandle'
+ ])
+ )
+ return
+ const tableColumn = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `Title`
+ }
+ },
+ children: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.SortHandle'
+ }
+ }
+ ]
+ })
+ ensureObjectItemsNode(node).prepend(tableColumn)
+ }
+ },
+ {
+ title: node.getMessage('addIndex'),
+ icon: 'AddIndex',
+ onClick: () => {
+ if (
+ hasNodeByComponentPath(node, [
+ 'ArrayTable',
+ '*',
+ 'ArrayTable.Column',
+ 'ArrayTable.Index'
+ ])
+ )
+ return
+ const tableColumn = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `序号`
+ }
+ },
+ children: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Index'
+ }
+ }
+ ]
+ })
+ const sortNode = findNodeByComponentPath(node, [
+ 'ArrayTable',
+ '*',
+ 'ArrayTable.Column',
+ 'ArrayTable.SortHandle'
+ ])
+ if (sortNode) {
+ sortNode.parent.insertAfter(tableColumn)
+ } else {
+ ensureObjectItemsNode(node).prepend(tableColumn)
+ }
+ }
+ },
+ {
+ title: node.getMessage('addColumn'),
+ icon: 'AddColumn',
+ onClick: () => {
+ const operationNode = findNodeByComponentPath(node, [
+ 'ArrayTable',
+ '*',
+ 'ArrayTable.Column',
+ (name) => {
+ return (
+ name === 'ArrayTable.Remove' ||
+ name === 'ArrayTable.MoveDown' ||
+ name === 'ArrayTable.MoveUp'
+ )
+ }
+ ])
+ const tableColumn = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `Title`
+ }
+ }
+ })
+ if (operationNode) {
+ operationNode.parent.insertBefore(tableColumn)
+ } else {
+ ensureObjectItemsNode(node).append(tableColumn)
+ }
+ }
+ },
+ {
+ title: node.getMessage('addOperation'),
+ icon: 'AddOperation',
+ onClick: () => {
+ const oldOperationNode = findNodeByComponentPath(node, [
+ 'ArrayTable',
+ '*',
+ 'ArrayTable.Column',
+ (name) => {
+ return (
+ name === 'ArrayTable.Remove' ||
+ name === 'ArrayTable.MoveDown' ||
+ name === 'ArrayTable.MoveUp'
+ )
+ }
+ ])
+ const oldAdditionNode = findNodeByComponentPath(node, [
+ 'ArrayTable',
+ 'ArrayTable.Addition'
+ ])
+ if (!oldOperationNode) {
+ const operationNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Column',
+ 'x-component-props': {
+ title: `操作`
+ }
+ },
+ children: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.Remove'
+ }
+ },
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.MoveDown'
+ }
+ },
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'ArrayTable.MoveUp'
+ }
+ }
+ ]
+ })
+ ensureObjectItemsNode(node).append(operationNode)
+ }
+ if (!oldAdditionNode) {
+ const additionNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ title: 'Addition',
+ 'x-component': 'ArrayTable.Addition'
+ }
+ })
+ ensureObjectItemsNode(node).insertAfter(additionNode)
+ }
+ }
+ }
+ ]}
+ />
+
+ )
+ }
+ }
+ })
+ ),
+ {
+ Behavior: createBehavior(createArrayBehavior('ArrayTable'), {
+ name: 'ArrayTable.Column',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'ArrayTable.Column',
+ designerProps: {
+ droppable: true,
+ allowDrop: (node) =>
+ node.props['type'] === 'object' && node.parent?.props?.['x-component'] === 'ArrayTable',
+ propsSchema: createVoidFieldSchema(AllSchemas.ArrayTable.Column)
+ },
+ designerLocales: AllLocales.ArrayTableColumn
+ }),
+ Resource: createResource({
+ icon: 'ArrayTableSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'array',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'ArrayTable'
+ }
+ }
+ ]
+ }),
+ Column: () => null,
+ Index: ArrayBase.Index,
+ SortHandle: ArrayBase.SortHandle,
+ Addition: ArrayBase.Addition,
+ Remove: ArrayBase.Remove,
+ MoveDown: ArrayBase.MoveDown,
+ MoveUp: ArrayBase.MoveUp,
+ useArray: ArrayBase.useArray,
+ useIndex: ArrayBase.useIndex,
+ useRecord: ArrayBase.useRecord
+ }
+)
diff --git a/packages/prototypes/src/components/ArrayTable/styles.less b/packages/prototypes/src/components/ArrayTable/styles.less
new file mode 100644
index 0000000..a637451
--- /dev/null
+++ b/packages/prototypes/src/components/ArrayTable/styles.less
@@ -0,0 +1,5 @@
+@import '../../styles.less';
+
+.dn-array-table {
+ background-color: @component-background;
+}
diff --git a/packages/prototypes/src/components/Card/index.ts b/packages/prototypes/src/components/Card/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Card/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Card/preview.tsx b/packages/prototypes/src/components/Card/preview.tsx
new file mode 100644
index 0000000..3715366
--- /dev/null
+++ b/packages/prototypes/src/components/Card/preview.tsx
@@ -0,0 +1,62 @@
+import { defineComponent, unref } from 'vue'
+import { Card as AntCard } from 'ant-design-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { uid } from '@pind/designable-shared'
+import { useTreeNode, TreeNodeWidget, DroppableWidget } from '@formily/antdv-designable'
+import { createVoidFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Card = composeExport(
+ defineComponent({
+ props: { title: {} },
+ setup(props, { attrs }) {
+ const nodeRef = useTreeNode()
+
+ return () => {
+ const node = unref(nodeRef)
+
+ return (
+ {props.title}}
+ >
+ {node.children.length ? (
+ node.children.map((child) => )
+ ) : (
+
+ )}
+
+ )
+ }
+ }
+ }),
+ {
+ Behavior: createBehavior({
+ name: 'Card',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Card',
+ designerProps: {
+ droppable: true,
+ propsSchema: createVoidFieldSchema(AllSchemas.Card)
+ },
+ designerLocales: AllLocales.Card
+ }),
+ Resource: createResource({
+ icon: 'CardSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'Card',
+ 'x-component-props': {
+ title: 'Title'
+ }
+ }
+ }
+ ]
+ })
+ }
+)
diff --git a/packages/prototypes/src/components/Cascader/index.ts b/packages/prototypes/src/components/Cascader/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Cascader/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Cascader/preview.tsx b/packages/prototypes/src/components/Cascader/preview.tsx
new file mode 100644
index 0000000..56550da
--- /dev/null
+++ b/packages/prototypes/src/components/Cascader/preview.tsx
@@ -0,0 +1,65 @@
+import { Cascader as FormilyCascader } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Cascader = composeExport(FormilyCascader, {
+ Behavior: createBehavior({
+ name: 'Cascader',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Cascader',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Cascader)
+ },
+ designerLocales: AllLocales.Cascader
+ }),
+ Resource: createResource({
+ icon: 'CascaderSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ title: 'Cascader',
+ enum: [
+ {
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ children: [
+ {
+ value: 'hangzhou',
+ label: 'Hangzhou',
+ children: [
+ {
+ value: 'xihu',
+ label: 'West Lake'
+ }
+ ]
+ }
+ ]
+ },
+ {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ children: [
+ {
+ value: 'nanjing',
+ label: 'Nanjing',
+ children: [
+ {
+ value: 'zhonghuamen',
+ label: 'Zhong Hua Men'
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Cascader'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Checkbox/index.ts b/packages/prototypes/src/components/Checkbox/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Checkbox/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Checkbox/preview.tsx b/packages/prototypes/src/components/Checkbox/preview.tsx
new file mode 100644
index 0000000..325a10b
--- /dev/null
+++ b/packages/prototypes/src/components/Checkbox/preview.tsx
@@ -0,0 +1,36 @@
+import { Checkbox as FormilyCheckbox } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Checkbox = composeExport(FormilyCheckbox, {
+ Behavior: createBehavior({
+ name: 'Checkbox.Group',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Checkbox.Group',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Checkbox.Group)
+ },
+ designerLocales: AllLocales.CheckboxGroup
+ }),
+ Resource: createResource({
+ icon: 'CheckboxGroupSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'array' || 'Array',
+ title: 'Checkbox Group',
+ enum: [
+ { label: '选项1', value: 1 },
+ { label: '选项2', value: 2 }
+ ],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox.Group'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/DatePicker/index.ts b/packages/prototypes/src/components/DatePicker/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/DatePicker/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/DatePicker/preview.tsx b/packages/prototypes/src/components/DatePicker/preview.tsx
new file mode 100644
index 0000000..43446e0
--- /dev/null
+++ b/packages/prototypes/src/components/DatePicker/preview.tsx
@@ -0,0 +1,59 @@
+import { DatePicker as FormilyDatePicker } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const DatePicker = composeExport(FormilyDatePicker, {
+ Behavior: createBehavior(
+ {
+ name: 'DatePicker',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'DatePicker',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.DatePicker)
+ },
+ designerLocales: AllLocales.DatePicker
+ },
+ {
+ name: 'DateRangePicker',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'DatePicker.RangePicker',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.DatePicker.RangePicker)
+ },
+ designerLocales: AllLocales.DateRangePicker
+ }
+ ),
+ Resource: createResource(
+ {
+ icon: 'DatePickerSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'string',
+ title: 'DatePicker',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'DatePicker'
+ }
+ }
+ ]
+ },
+ {
+ icon: 'DateRangePickerSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'string',
+ title: 'DateRangePicker',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'DatePicker.RangePicker'
+ }
+ }
+ ]
+ }
+ )
+})
diff --git a/packages/prototypes/src/components/Field/index.ts b/packages/prototypes/src/components/Field/index.ts
new file mode 100644
index 0000000..7eb57a8
--- /dev/null
+++ b/packages/prototypes/src/components/Field/index.ts
@@ -0,0 +1,2 @@
+export * from './preview'
+export * from './shared'
diff --git a/packages/prototypes/src/components/Field/preview.tsx b/packages/prototypes/src/components/Field/preview.tsx
new file mode 100644
index 0000000..beaf71c
--- /dev/null
+++ b/packages/prototypes/src/components/Field/preview.tsx
@@ -0,0 +1,176 @@
+import { createBehavior } from '@pind/designable-core'
+import { isArr, isFn, isStr } from '@pind/designable-shared'
+import { FormPath } from '@formily/core'
+import { toJS } from '@formily/reactive'
+import { observer } from '@formily/reactive-vue'
+import { each, reduce } from '@formily/shared'
+import type { ISchema } from '@formily/vue'
+import {
+ ArrayField,
+ h as CreateElement,
+ Field as InternalField,
+ ObjectField,
+ Schema,
+ VoidField
+} from '@formily/vue'
+import { FormItem } from '@formily/antdv'
+import { useComponents, useDesigner, useTreeNode } from '@formily/antdv-designable'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent } from 'vue'
+import { Container } from '../../common/Container'
+import { AllLocales } from '../../locales'
+
+Schema.silent(true)
+
+const SchemaStateMap = {
+ title: 'title',
+ description: 'description',
+ default: 'value',
+ enum: 'dataSource',
+ readOnly: 'readOnly',
+ writeOnly: 'editable',
+ required: 'required',
+ 'x-content': 'content',
+ 'x-value': 'value',
+ 'x-editable': 'editable',
+ 'x-disabled': 'disabled',
+ 'x-read-pretty': 'readPretty',
+ 'x-read-only': 'readOnly',
+ 'x-visible': 'visible',
+ 'x-hidden': 'hidden',
+ 'x-display': 'display',
+ 'x-pattern': 'pattern'
+}
+
+const NeedShownExpression = {
+ title: true,
+ description: true,
+ default: true,
+ 'x-content': true,
+ 'x-value': true
+}
+
+const isExpression = (val: any) => isStr(val) && /^\{\{.*\}\}$/.test(val)
+
+const filterExpression = (val: any) => {
+ if (typeof val === 'object') {
+ const isArray = isArr(val)
+ const results = reduce(
+ val,
+ (buf: any, value, key) => {
+ if (isExpression(value)) {
+ return buf
+ } else {
+ const results = filterExpression(value)
+ if (results === undefined || results === null) return buf
+ if (isArray) {
+ return buf.concat([results])
+ }
+ buf[key] = results
+ return buf
+ }
+ },
+ isArray ? [] : {}
+ )
+ return results
+ }
+ if (isExpression(val)) {
+ return
+ }
+ return val
+}
+
+const toDesignableFieldProps = (
+ schema: ISchema,
+ components: any,
+ nodeIdAttrName: string,
+ id: string
+) => {
+ const results: any = {}
+ each(SchemaStateMap, (fieldKey, schemaKey) => {
+ const value = schema[schemaKey]
+ if (isExpression(value)) {
+ if (!NeedShownExpression[schemaKey]) return
+ if (value) {
+ results[fieldKey] = value
+ return
+ }
+ } else if (value) {
+ results[fieldKey] = filterExpression(value)
+ }
+ })
+ if (!components['FormItem']) {
+ components['FormItem'] = FormItem
+ }
+ const decorator = schema['x-decorator'] && FormPath.getIn(components, schema['x-decorator'])
+ const component = schema['x-component'] && FormPath.getIn(components, schema['x-component'])
+ const decoratorProps = schema['x-decorator-props'] || {}
+ const componentProps = schema['x-component-props'] || {}
+
+ if (decorator) {
+ results.decorator = [decorator, toJS(decoratorProps)]
+ }
+ if (component) {
+ // 有的是functional 有的是 正常的 vueComponent
+ results.component = [
+ isFn(component)
+ ? component
+ : Object.assign({}, component, { Behavior: null, Resource: null }),
+ toJS(componentProps)
+ ]
+ }
+ if (decorator) {
+ FormPath.setIn(results['decorator'][1], nodeIdAttrName, id)
+ } else if (component) {
+ FormPath.setIn(results['component'][1], nodeIdAttrName, id)
+ }
+ // vue为异步渲染需要进行缓存 不然就变成了函数
+ const title = results.title
+ results.title = results.title && (() => {title})
+ // TODO::formily vue 貌似不支持呢
+ results.description = results.description // (() => {description})
+ return results
+}
+//
+const FieldComponent = observer(
+ defineComponent({
+ name: 'DnField',
+ setup(_props, { attrs, slots }) {
+ const designerRef = useDesigner()
+ const componentsRef = useComponents()
+ const nodeRef = useTreeNode()
+ const props = attrs as ISchema
+ return () => {
+ if (!nodeRef.value) return null
+ const fieldProps = toDesignableFieldProps(
+ props,
+ componentsRef.value,
+ designerRef.value.props.nodeIdAttrName,
+ nodeRef.value.id
+ )
+ if (props.type === 'object') {
+ return (
+
+
+ {slots.default?.()}
+
+
+ )
+ } else if (props.type === 'array') {
+ return CreateElement(ArrayField, { ...fieldProps, name: nodeRef.value.id }, slots)
+ } else if (nodeRef.value.props.type === 'void') {
+ return CreateElement(VoidField, { ...fieldProps, name: nodeRef.value.id }, slots)
+ }
+ return CreateElement(InternalField, { ...fieldProps, name: nodeRef.value.id }, {})
+ }
+ }
+ })
+)
+
+export const Field = composeExport(FieldComponent, {
+ Behavior: createBehavior({
+ name: 'Field',
+ selector: 'Field',
+ designerLocales: AllLocales.Field
+ })
+})
diff --git a/packages/prototypes/src/components/Field/shared.ts b/packages/prototypes/src/components/Field/shared.ts
new file mode 100644
index 0000000..b9e62a1
--- /dev/null
+++ b/packages/prototypes/src/components/Field/shared.ts
@@ -0,0 +1,230 @@
+import type { ISchema } from '@formily/json-schema'
+import { DataSourceSetter, ReactionsSetter, ValidatorSetter } from '@formily/antdv-setters'
+import { FormItemSwitcher } from '../../common/FormItemSwitcher'
+import { AllSchemas } from '../../schemas'
+
+// TODO::setter没做
+export const createComponentSchema = (component: ISchema, decorator: ISchema) => {
+ return {
+ 'component-group': component && {
+ type: 'void',
+ 'x-component': 'CollapseItem',
+ 'x-reactions': {
+ fulfill: {
+ state: {
+ visible: '{{!!$form.values["x-component"]}}'
+ }
+ }
+ },
+ properties: {
+ 'x-component-props': component
+ }
+ },
+ 'decorator-group': decorator && {
+ type: 'void',
+ 'x-component': 'CollapseItem',
+ 'x-component-props': { defaultExpand: false },
+ 'x-reactions': {
+ fulfill: {
+ state: {
+ visible: '{{!!$form.values["x-decorator"]}}'
+ }
+ }
+ },
+ properties: {
+ 'x-decorator-props': decorator
+ }
+ },
+ 'component-style-group': {
+ type: 'void',
+ 'x-component': 'CollapseItem',
+ 'x-component-props': { defaultExpand: false },
+ 'x-reactions': {
+ fulfill: {
+ state: {
+ visible: '{{!!$form.values["x-component"]}}'
+ }
+ }
+ },
+ properties: {
+ 'x-component-props.style': AllSchemas.CSSStyle
+ }
+ },
+ 'decorator-style-group': {
+ type: 'void',
+ 'x-component': 'CollapseItem',
+ 'x-component-props': { defaultExpand: false },
+ 'x-reactions': {
+ fulfill: {
+ state: {
+ visible: '{{!!$form.values["x-decorator"]}}'
+ }
+ }
+ },
+ properties: {
+ 'x-decorator-props.style': AllSchemas.CSSStyle
+ }
+ }
+ }
+}
+
+export const createFieldSchema = (
+ component?: ISchema,
+ decorator: ISchema = AllSchemas.FormItem
+): ISchema => {
+ return {
+ type: 'object',
+ properties: {
+ 'field-group': {
+ type: 'void',
+ 'x-component': 'CollapseItem',
+ properties: {
+ name: {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input',
+ 'x-component-props': {
+ allowClear: true
+ }
+ },
+ title: {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input',
+ 'x-component-props': {
+ allowClear: true
+ }
+ },
+ description: {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input.TextArea',
+ 'x-component-props': {
+ rows: 1
+ }
+ },
+ 'x-display': {
+ default: 'visible',
+ type: 'string',
+ enum: ['visible', 'hidden', 'none', ''],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Select',
+ 'x-component-props': {}
+ },
+ 'x-pattern': {
+ default: 'editable',
+ type: 'string',
+ enum: ['editable', 'disabled', 'readOnly', 'readPretty', ''],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Select',
+ 'x-component-props': {}
+ },
+ default: {
+ 'x-decorator': 'FormItem',
+ 'x-component': 'ValueInput'
+ },
+ enum: {
+ 'x-decorator': 'FormItem',
+ 'x-component': DataSourceSetter
+ },
+ 'x-reactions': {
+ 'x-decorator': 'FormItem',
+ 'x-component': ReactionsSetter
+ },
+ 'x-validator': {
+ type: 'array',
+ 'x-component': ValidatorSetter
+ },
+ required: {
+ type: 'boolean',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Switch'
+ }
+ }
+ },
+ ...createComponentSchema(component, decorator)
+ }
+ }
+}
+
+export const createVoidFieldSchema = (
+ component?: ISchema,
+ decorator: ISchema = AllSchemas.FormItem
+) => {
+ return {
+ type: 'object',
+ properties: {
+ 'field-group': {
+ type: 'void',
+ 'x-component': 'CollapseItem',
+ properties: {
+ name: {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input',
+ 'x-component-props': {
+ allowClear: true
+ }
+ },
+ title: {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input',
+ 'x-component-props': {
+ allowClear: true
+ },
+ 'x-reactions': {
+ fulfill: {
+ state: {
+ hidden: '{{$form.values["x-decorator"] === "FormItem"}}'
+ }
+ }
+ }
+ },
+ description: {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input.TextArea',
+ 'x-reactions': {
+ fulfill: {
+ state: {
+ hidden: '{{$form.values["x-decorator"] === "FormItem"}}'
+ }
+ }
+ }
+ },
+ 'x-display': {
+ default: 'visible',
+ type: 'string',
+ enum: ['visible', 'hidden', 'none', ''],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Select',
+ 'x-component-props': {
+ allowClear: true
+ }
+ },
+ 'x-pattern': {
+ default: 'editable',
+ type: 'string',
+ enum: ['editable', 'disabled', 'readOnly', 'readPretty', ''],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Select',
+ 'x-component-props': {
+ allowClear: true
+ }
+ },
+ 'x-reactions': {
+ 'x-decorator': 'FormItem',
+ 'x-component': ReactionsSetter
+ },
+ 'x-decorator': {
+ type: 'string',
+ 'x-decorator': 'FormItem',
+ 'x-component': FormItemSwitcher
+ }
+ }
+ },
+ ...createComponentSchema(component, decorator)
+ }
+ }
+}
diff --git a/packages/prototypes/src/components/Form/index.tsx b/packages/prototypes/src/components/Form/index.tsx
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Form/index.tsx
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Form/preview.tsx b/packages/prototypes/src/components/Form/preview.tsx
new file mode 100644
index 0000000..cf42200
--- /dev/null
+++ b/packages/prototypes/src/components/Form/preview.tsx
@@ -0,0 +1,76 @@
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createForm } from '@formily/core'
+import { observer } from '@formily/reactive-vue'
+import { Form as FormilyForm } from '@formily/antdv'
+import { usePrefix } from '@formily/antdv-designable'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { computed, defineComponent, unref } from 'vue'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+import './styles.less'
+
+const FormComponent = observer(
+ defineComponent({
+ name: 'DnForm',
+ setup(props, { slots, attrs }) {
+ const prefix = usePrefix('designable-form')
+ const formRef = computed(() =>
+ createForm({
+ designable: true
+ })
+ )
+ return () => {
+ const form = unref(formRef)
+
+ return (
+
+ {slots.default?.()}
+
+ )
+ }
+ }
+ })
+)
+
+export const Form = composeExport(FormComponent, {
+ Behavior: createBehavior({
+ name: 'Form',
+ selector: (node) => node.componentName === 'Form',
+ designerProps(node) {
+ return {
+ draggable: !node.isRoot,
+ cloneable: !node.isRoot,
+ deletable: !node.isRoot,
+ droppable: true,
+ propsSchema: AllSchemas.Form,
+ defaultProps: {
+ labelCol: 6,
+ wrapperCol: 12,
+ colon: false,
+ feedbackLayout: 'loose',
+ size: 'default',
+ layout: 'horizontal',
+ tooltipLayout: 'icon',
+ labelAlign: 'right',
+ wrapperAlign: 'left',
+ shallow: true,
+ bordered: true
+ }
+ }
+ },
+ designerLocales: AllLocales.Form
+ }),
+ Resource: createResource({
+ title: { 'zh-CN': '表单', 'en-US': 'Form' },
+ icon: 'FormLayoutSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'object',
+ 'x-component': 'Form'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Form/styles.less b/packages/prototypes/src/components/Form/styles.less
new file mode 100644
index 0000000..ea00e9a
--- /dev/null
+++ b/packages/prototypes/src/components/Form/styles.less
@@ -0,0 +1,42 @@
+@import (reference) '../../styles.less';
+
+.dn-designable-form {
+ width: 100%;
+ height: 100%;
+ min-height: 80vh;
+
+ .@{ant-prefix}-input,
+ .@{ant-prefix}-input-number,
+ .@{ant-prefix}-input-affix-wrapper,
+ .@{ant-prefix}-cascader-picker,
+ .@{ant-prefix}-picker-input,
+ .@{ant-prefix}-picker,
+ .@{ant-prefix}-time-picker,
+ .@{ant-prefix}-calendar-picker,
+ .@{ant-prefix}-cascader-picker-label,
+ .@{ant-prefix}-slider,
+ .@{ant-prefix}-checkbox,
+ .@{ant-prefix}-cascader,
+ .@{ant-prefix}-textarea,
+ .@{ant-prefix}-rate,
+ .@{ant-prefix}-switch,
+ .@{ant-prefix}-radio,
+ .@{ant-prefix}-radio-wrapper,
+ .@{ant-prefix}-checkbox-group,
+ .@{ant-prefix}-checkbox-wrapper,
+ .@{ant-prefix}-radio-group,
+ .@{ant-prefix}-upload,
+ .@{ant-prefix}-transfer,
+ .@{ant-prefix}-select,
+ .@{ant-prefix}-select-selector {
+ pointer-events: none !important;
+
+ input {
+ pointer-events: none !important;
+ }
+ }
+
+ .ant-icon svg {
+ pointer-events: none;
+ }
+}
diff --git a/packages/prototypes/src/components/FormCollapse/index.ts b/packages/prototypes/src/components/FormCollapse/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/FormCollapse/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/FormCollapse/preview.tsx b/packages/prototypes/src/components/FormCollapse/preview.tsx
new file mode 100644
index 0000000..ff778a2
--- /dev/null
+++ b/packages/prototypes/src/components/FormCollapse/preview.tsx
@@ -0,0 +1,210 @@
+import { observer } from '@formily/reactive-vue'
+import { Collapse } from 'ant-design-vue'
+import { TreeNode, createBehavior, createResource } from '@pind/designable-core'
+import {
+ useTreeNode,
+ useNodeIdProps,
+ DroppableWidget,
+ TreeNodeWidget,
+ useSelection
+} from '@formily/antdv-designable'
+import { toArr } from '@formily/shared'
+import { defineComponent, nextTick, ref } from 'vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { LoadTemplate } from '../../common/LoadTemplate'
+import { useDropTemplate } from '../../hooks'
+import { createVoidFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+import { matchComponent } from '../../shared'
+
+const parseCollapse = (parent: TreeNode) => {
+ const tabs: TreeNode[] = []
+ parent.children.forEach((node) => {
+ if (matchComponent(node, 'FormCollapse.CollapsePanel')) {
+ tabs.push(node)
+ }
+ })
+ return tabs
+}
+// CollapsePanel: FragmentComponent,
+// & {
+// CollapsePanel?: VueComponent
+// }
+export const FormCollapse = composeExport(
+ observer(
+ defineComponent({
+ name: 'DnFormCollapse',
+ props: { accordion: Boolean },
+ setup(props, { attrs }) {
+ const activeKeyRef = ref([])
+
+ const setActiveKey = (value) => {
+ activeKeyRef.value = value
+ }
+ const nodeRef = useTreeNode()
+ const nodeIdRef = useNodeIdProps()
+ const selectionRef = useSelection()
+
+ const designerRef = useDropTemplate('FormCollapse', (source) => {
+ const panelNode = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormCollapse.CollapsePanel',
+ 'x-component-props': {
+ header: `Unnamed Title`
+ }
+ },
+ children: source
+ })
+
+ setActiveKey(toArr(activeKeyRef.value).concat(panelNode.id))
+ return [panelNode]
+ })
+
+ const getCorrectActiveKey = (activeKey: string[] | string, tabs: TreeNode[]) => {
+ if (!tabs.length || !activeKey?.length) {
+ if (props.accordion) {
+ return tabs[0]?.id
+ }
+ return tabs.map((item) => item.id)
+ }
+ if (
+ tabs.some((node) =>
+ Array.isArray(activeKey) ? activeKey.includes(node.id) : node.id === activeKey
+ )
+ ) {
+ return props.accordion ? activeKey[activeKey.length - 1] : activeKey
+ }
+ return props.accordion ? tabs[tabs.length - 1].id : [tabs[tabs.length - 1].id]
+ }
+
+ return () => {
+ const node = nodeRef.value
+ const nodeId = nodeIdRef.value
+ const activeKey = activeKeyRef.value
+ const designer = designerRef.value
+
+ const panels = parseCollapse(node)
+
+ const renderCollapse = () => {
+ if (!node.children?.length) return
+ return (
+
+ {panels.map((panel) => {
+ const props = panel.props['x-component-props'] || {}
+ return (
+
+ {props.header}
+
+ }
+ >
+
+ {/* TODO::reactive的flow,重新渲染了整个子树 */}
+ {panel.children.length ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ })}
+
+ )
+ }
+ return (
+
+ {renderCollapse()}
+ {
+ const tabPane = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormCollapse.CollapsePanel',
+ 'x-component-props': {
+ header: `Unnamed Title`
+ }
+ }
+ })
+ node.append(tabPane)
+ const keys = toArr(activeKey)
+ setActiveKey(keys.concat(tabPane.id))
+ nextTick(() => {
+ selectionRef.value.select(tabPane.id)
+ })
+ }
+ }
+ ]}
+ />
+
+ )
+ }
+ }
+ })
+ ),
+ {
+ Behavior: createBehavior(
+ {
+ name: 'FormCollapse',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'FormCollapse',
+ designerProps: {
+ droppable: true,
+ allowAppend: (target, source) =>
+ target.children.length === 0 ||
+ source.every((node) => node.props['x-component'] === 'FormCollapse.CollapsePanel'),
+ propsSchema: createVoidFieldSchema(AllSchemas.FormCollapse)
+ },
+ designerLocales: AllLocales.FormCollapse
+ },
+ {
+ name: 'FormCollapse.CollapsePanel',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'FormCollapse.CollapsePanel',
+ designerProps: {
+ droppable: true,
+ allowDrop: (node) => node.props['x-component'] === 'FormCollapse',
+ propsSchema: createVoidFieldSchema(AllSchemas.FormCollapse.CollapsePanel)
+ },
+ designerLocales: AllLocales.FormCollapsePanel
+ }
+ ),
+ Resource: createResource({
+ icon: 'CollapseSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormCollapse'
+ }
+ }
+ ]
+ })
+ }
+)
diff --git a/packages/prototypes/src/components/FormGrid/index.ts b/packages/prototypes/src/components/FormGrid/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/FormGrid/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/FormGrid/preview.tsx b/packages/prototypes/src/components/FormGrid/preview.tsx
new file mode 100644
index 0000000..6f3edfa
--- /dev/null
+++ b/packages/prototypes/src/components/FormGrid/preview.tsx
@@ -0,0 +1,130 @@
+import { FormGrid as FormilyGird } from '@formily/antdv'
+import { TreeNode, createBehavior, createResource } from '@pind/designable-core'
+import { useTreeNode, useNodeIdProps, DroppableWidget } from '@formily/antdv-designable'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent } from 'vue'
+import { LoadTemplate } from '../../common/LoadTemplate'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+import './styles.less'
+
+export const FormGrid = composeExport(
+ defineComponent({
+ inheritAttrs: false,
+ setup(props, { slots, attrs }) {
+ const nodeRef = useTreeNode()
+ const nodeIdRef = useNodeIdProps()
+
+ return () => {
+ if (nodeRef.value.children.length === 0) return
+ const totalColumns = nodeRef.value.children.reduce(
+ (buf, child) => buf + (child.props?.['x-component-props']?.gridSpan ?? 1),
+ 0
+ )
+ return (
+
+
+ {slots.default?.()}
+
+ {
+ const column = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormGrid.GridColumn'
+ }
+ })
+ nodeRef.value.append(column)
+ }
+ }
+ ]}
+ />
+
+ )
+ }
+ }
+ }),
+ {
+ GridColumn: defineComponent({
+ props: { gridSpan: { default: 1 } },
+ setup(props, { attrs, slots }) {
+ return () => {
+ return (
+
+ {slots.default?.()}
+
+ )
+ }
+ }
+ }),
+ Behavior: createBehavior(
+ {
+ name: 'FormGrid',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'FormGrid',
+ designerProps: {
+ droppable: true,
+ allowDrop: (node) => node.props['x-component'] !== 'FormGrid',
+ propsSchema: createFieldSchema(AllSchemas.FormGrid)
+ },
+ designerLocales: AllLocales.FormGrid
+ },
+ {
+ name: 'FormGrid.GridColumn',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'FormGrid.GridColumn',
+ designerProps: {
+ droppable: true,
+ allowDrop: (node) => node.props['x-component'] === 'FormGrid',
+ propsSchema: createFieldSchema(AllSchemas.FormGrid.GridColumn)
+ },
+ designerLocales: AllLocales.FormGridColumn
+ }
+ ),
+ Resource: createResource({
+ icon: 'GridSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormGrid'
+ },
+ children: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormGrid.GridColumn'
+ }
+ },
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormGrid.GridColumn'
+ }
+ },
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormGrid.GridColumn'
+ }
+ }
+ ]
+ }
+ ]
+ })
+ }
+)
diff --git a/packages/prototypes/src/components/FormGrid/styles.less b/packages/prototypes/src/components/FormGrid/styles.less
new file mode 100644
index 0000000..7355667
--- /dev/null
+++ b/packages/prototypes/src/components/FormGrid/styles.less
@@ -0,0 +1,5 @@
+.dn-grid-column {
+ margin: 4px;
+ min-height: 60px;
+ border: 1px dashed #aaa;
+}
diff --git a/packages/prototypes/src/components/FormLayout/index.ts b/packages/prototypes/src/components/FormLayout/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/FormLayout/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/FormLayout/preview.ts b/packages/prototypes/src/components/FormLayout/preview.ts
new file mode 100644
index 0000000..0de6752
--- /dev/null
+++ b/packages/prototypes/src/components/FormLayout/preview.ts
@@ -0,0 +1,32 @@
+import { FormLayout as FormilyFormLayout } from '@formily/antdv'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { withContainer } from '../../common/Container'
+import { createVoidFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const FormLayout = composeExport(withContainer(FormilyFormLayout), {
+ Behavior: createBehavior({
+ name: 'FormLayout',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'FormLayout',
+ designerProps: {
+ droppable: true,
+ propsSchema: createVoidFieldSchema(AllSchemas.FormLayout)
+ },
+ designerLocales: AllLocales.FormLayout
+ }),
+ Resource: createResource({
+ icon: 'FormLayoutSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormLayout'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/FormTab/index.ts b/packages/prototypes/src/components/FormTab/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/FormTab/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/FormTab/preview.tsx b/packages/prototypes/src/components/FormTab/preview.tsx
new file mode 100644
index 0000000..53aaf2f
--- /dev/null
+++ b/packages/prototypes/src/components/FormTab/preview.tsx
@@ -0,0 +1,193 @@
+// import React, { Fragment, useState } from 'react'
+import { createBehavior, createResource, TreeNode } from '@pind/designable-core'
+import { uid } from '@pind/designable-shared'
+import { observer } from '@formily/reactive-vue'
+import {
+ DroppableWidget,
+ TreeNodeWidget,
+ useNodeIdProps,
+ useSelection,
+ useTreeNode
+} from '@formily/antdv-designable'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { Tabs } from 'ant-design-vue'
+import { defineComponent, ref } from 'vue'
+import { LoadTemplate } from '../../common/LoadTemplate'
+import { useDropTemplate } from '../../hooks'
+import { AllLocales } from '../../locales'
+import { AllSchemas } from '../../schemas'
+import { matchComponent } from '../../shared'
+import { createVoidFieldSchema } from '../Field'
+// import type { Tabs as TabsProps } from 'ant-design-vue'
+
+const parseTabs = (parent: TreeNode) => {
+ const tabs: TreeNode[] = []
+ parent.children.forEach((node) => {
+ if (matchComponent(node, 'FormTab.TabPane')) {
+ tabs.push(node)
+ }
+ })
+ return tabs
+}
+
+const getCorrectActiveKey = (activeKey: string, tabs: TreeNode[]) => {
+ if (tabs.length === 0) return
+ if (tabs.some((node) => node.id === activeKey)) return activeKey
+ return tabs[tabs.length - 1].id
+}
+// {
+// TabPane?: VueComponent
+// }
+export const FormTab = composeExport(
+ observer(
+ defineComponent({
+ setup(props, { attrs }) {
+ const activeKeyRef = ref()
+ const nodeIdRef = useNodeIdProps()
+ const nodeRef = useTreeNode()
+ const selectionRef = useSelection()
+ const designerRef = useDropTemplate('FormTab', (source) => {
+ return [
+ new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormTab.TabPane',
+ 'x-component-props': {
+ label: `Unnamed Title`
+ }
+ },
+ children: source
+ })
+ ]
+ })
+
+ const setActiveKey = (value) => {
+ activeKeyRef.value = value
+ selectionRef.value.select(value)
+ }
+ return () => {
+ const activeKey = activeKeyRef.value
+ const nodeId = nodeIdRef.value
+ const node = nodeRef.value
+ const designer = designerRef.value
+
+ const tabs = parseTabs(node)
+ const renderTabs = () => {
+ if (!node.children?.length) return
+ return (
+ {
+ setActiveKey(id)
+ }}
+ activeKey={getCorrectActiveKey(activeKey, tabs)}
+ >
+ {tabs.map((tab) => {
+ const props = tab.props['x-component-props'] || {}
+ return (
+
+ {props.tab}
+
+ }
+ >
+
+ {tab.children.length ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ })}
+
+ )
+ }
+ return (
+
+ {renderTabs()}
+ {
+ const tabPane = new TreeNode({
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormTab.TabPane',
+ 'x-component-props': {
+ tab: `Unnamed Title`
+ }
+ }
+ })
+ node.append(tabPane)
+ setActiveKey(tabPane.id)
+ }
+ }
+ ]}
+ />
+
+ )
+ }
+ }
+ })
+ ),
+ {
+ Behavior: createBehavior(
+ {
+ name: 'FormTab',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'FormTab',
+ designerProps: {
+ droppable: true,
+ allowAppend: (target, source) =>
+ target.children.length === 0 ||
+ source.every((node) => node.props['x-component'] === 'FormTab.TabPane'),
+ propsSchema: createVoidFieldSchema(AllSchemas.FormTab)
+ },
+ designerLocales: AllLocales.FormTab
+ },
+ {
+ name: 'FormTab.TabPane',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'FormTab.TabPane',
+ designerProps: {
+ droppable: true,
+ allowDrop: (node) => node.props['x-component'] === 'FormTab',
+ propsSchema: createVoidFieldSchema(AllSchemas.FormTab.TabPane)
+ },
+ designerLocales: AllLocales.FormTabPane
+ }
+ ),
+ Resource: createResource({
+ icon: 'TabSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'FormTab'
+ }
+ }
+ ]
+ })
+ }
+)
diff --git a/packages/prototypes/src/components/Input/index.ts b/packages/prototypes/src/components/Input/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Input/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Input/preview.ts b/packages/prototypes/src/components/Input/preview.ts
new file mode 100644
index 0000000..af623ea
--- /dev/null
+++ b/packages/prototypes/src/components/Input/preview.ts
@@ -0,0 +1,63 @@
+import { createBehavior, createResource } from '@pind/designable-core'
+import { Input as FormilyInput } from '@formily/antdv'
+import type { DnFC } from '@formily/antdv-designable'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { AllLocales } from '../../locales'
+import { AllSchemas } from '../../schemas'
+import { createFieldSchema } from '../Field'
+
+export const Input: DnFC> = composeExport(
+ FormilyInput,
+ {
+ Behavior: createBehavior(
+ {
+ name: 'Input',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Input',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Input)
+ },
+ designerLocales: AllLocales.Input
+ },
+ {
+ name: 'Input.TextArea',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Input.TextArea',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Input.TextArea)
+ },
+ designerLocales: AllLocales.TextArea
+ }
+ ),
+ Resource: createResource(
+ {
+ icon: 'InputSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'string',
+ title: 'Input',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input'
+ }
+ }
+ ]
+ },
+ {
+ icon: 'TextAreaSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'string',
+ title: 'TextArea',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input.TextArea'
+ }
+ }
+ ]
+ }
+ )
+ }
+)
diff --git a/packages/prototypes/src/components/NumberPicker/index.ts b/packages/prototypes/src/components/NumberPicker/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/NumberPicker/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/NumberPicker/preview.ts b/packages/prototypes/src/components/NumberPicker/preview.ts
new file mode 100644
index 0000000..a2ac2dd
--- /dev/null
+++ b/packages/prototypes/src/components/NumberPicker/preview.ts
@@ -0,0 +1,37 @@
+import { InputNumber as FormilyInputNumber } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+const NumberPicker = composeExport(FormilyInputNumber, {
+ Behavior: createBehavior({
+ name: 'InputNumber',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'InputNumber',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.InputNumber)
+ },
+ designerLocales: AllLocales.InputNumber
+ }),
+ Resource: createResource({
+ icon: 'NumberPickerSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'number',
+ title: 'InputNumber',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'InputNumber',
+ 'x-component-props': {
+ 'controls-position': 'right'
+ }
+ }
+ }
+ ]
+ })
+})
+
+export const InputNumber = NumberPicker
diff --git a/packages/prototypes/src/components/Object/index.ts b/packages/prototypes/src/components/Object/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Object/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Object/preview.tsx b/packages/prototypes/src/components/Object/preview.tsx
new file mode 100644
index 0000000..70de326
--- /dev/null
+++ b/packages/prototypes/src/components/Object/preview.tsx
@@ -0,0 +1,29 @@
+import { createBehavior, createResource } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createFieldSchema } from '../Field'
+import { Container } from '../../common/Container'
+import { AllLocales } from '../../locales'
+
+export const ObjectContainer = composeExport(Container, {
+ Behavior: createBehavior({
+ name: 'Object',
+ extends: ['Field'],
+ selector: (node) => node.props.type === 'object',
+ designerProps: {
+ droppable: true,
+ propsSchema: createFieldSchema()
+ },
+ designerLocales: AllLocales.ObjectLocale
+ }),
+ Resource: createResource({
+ icon: 'ObjectSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'object'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Password/index.ts b/packages/prototypes/src/components/Password/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Password/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Password/preview.tsx b/packages/prototypes/src/components/Password/preview.tsx
new file mode 100644
index 0000000..ca215ee
--- /dev/null
+++ b/packages/prototypes/src/components/Password/preview.tsx
@@ -0,0 +1,39 @@
+import { Password as FormilyPassword } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { merge } from '@formily/shared'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Password = composeExport(FormilyPassword, {
+ Behavior: createBehavior({
+ name: 'Password',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Password',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Input)
+ },
+ designerLocales: merge(AllLocales.Input, {
+ 'zh-CN': {
+ title: '密码输入'
+ },
+ 'en-US': {
+ title: 'PassWord'
+ }
+ })
+ }),
+ Resource: createResource({
+ icon: 'PasswordSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ title: 'Password',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Password'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Radio/index.ts b/packages/prototypes/src/components/Radio/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Radio/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Radio/preview.tsx b/packages/prototypes/src/components/Radio/preview.tsx
new file mode 100644
index 0000000..93b12bc
--- /dev/null
+++ b/packages/prototypes/src/components/Radio/preview.tsx
@@ -0,0 +1,36 @@
+import { Radio as FormilyRadio } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Radio = composeExport(FormilyRadio, {
+ Behavior: createBehavior({
+ name: 'Radio.Group',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Radio.Group',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Radio.Group)
+ },
+ designerLocales: AllLocales.RadioGroup
+ }),
+ Resource: createResource({
+ icon: 'RadioGroupSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'string | number',
+ title: 'Radio Group',
+ enum: [
+ { label: '选项1', value: 1 },
+ { label: '选项2', value: 2 }
+ ],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Radio.Group'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Rate/index.ts b/packages/prototypes/src/components/Rate/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Rate/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Rate/preview.tsx b/packages/prototypes/src/components/Rate/preview.tsx
new file mode 100644
index 0000000..328b450
--- /dev/null
+++ b/packages/prototypes/src/components/Rate/preview.tsx
@@ -0,0 +1,32 @@
+import { Rate as AntRate } from 'ant-design-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Rate = composeExport(AntRate, {
+ Behavior: createBehavior({
+ name: 'Rate',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Rate',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Rate)
+ },
+ designerLocales: AllLocales.Rate
+ }),
+ Resource: createResource({
+ icon: 'RateSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'number',
+ title: 'Rate',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Rate'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Select/index.ts b/packages/prototypes/src/components/Select/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Select/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Select/preview.tsx b/packages/prototypes/src/components/Select/preview.tsx
new file mode 100644
index 0000000..7f9065b
--- /dev/null
+++ b/packages/prototypes/src/components/Select/preview.tsx
@@ -0,0 +1,31 @@
+import { Select as FormilySelect } from '@formily/antdv'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Select = composeExport(FormilySelect, {
+ Behavior: createBehavior({
+ name: 'Select',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Select',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Select)
+ },
+ designerLocales: AllLocales.Select
+ }),
+ Resource: createResource({
+ icon: 'SelectSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ title: 'Select',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Select'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Slider/index.ts b/packages/prototypes/src/components/Slider/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Slider/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Slider/preview.tsx b/packages/prototypes/src/components/Slider/preview.tsx
new file mode 100644
index 0000000..2518593
--- /dev/null
+++ b/packages/prototypes/src/components/Slider/preview.tsx
@@ -0,0 +1,32 @@
+import { Slider as AntSlider } from 'ant-design-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Slider = composeExport(AntSlider, {
+ Behavior: createBehavior({
+ name: 'Slider',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Slider',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Slider)
+ },
+ designerLocales: AllLocales.Slider
+ }),
+ Resource: createResource({
+ icon: 'SliderSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'number',
+ title: 'Slider',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Slider'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Space/index.ts b/packages/prototypes/src/components/Space/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Space/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Space/preview.tsx b/packages/prototypes/src/components/Space/preview.tsx
new file mode 100644
index 0000000..103bb11
--- /dev/null
+++ b/packages/prototypes/src/components/Space/preview.tsx
@@ -0,0 +1,33 @@
+import { Space as FormilySpace } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createVoidFieldSchema } from '../Field'
+import { withContainer } from '../../common/Container'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Space = composeExport(withContainer(FormilySpace), {
+ Behavior: createBehavior({
+ name: 'Space',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Space',
+ designerProps: {
+ droppable: true,
+ inlineChildrenLayout: true,
+ propsSchema: createVoidFieldSchema(AllSchemas.Space)
+ },
+ designerLocales: AllLocales.Space
+ }),
+ Resource: createResource({
+ icon: 'SpaceSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'void',
+ 'x-component': 'Space'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Switch/index.ts b/packages/prototypes/src/components/Switch/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Switch/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Switch/preview.tsx b/packages/prototypes/src/components/Switch/preview.tsx
new file mode 100644
index 0000000..915dc1c
--- /dev/null
+++ b/packages/prototypes/src/components/Switch/preview.tsx
@@ -0,0 +1,32 @@
+import { Switch as AntSwitch } from 'ant-design-vue'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Switch = composeExport(AntSwitch, {
+ Behavior: createBehavior({
+ name: 'Switch',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Switch',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Switch)
+ },
+ designerLocales: AllLocales.Switch
+ }),
+ Resource: createResource({
+ icon: 'SwitchSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'boolean',
+ title: 'Switch',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Switch'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Text/index.ts b/packages/prototypes/src/components/Text/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Text/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Text/preview.tsx b/packages/prototypes/src/components/Text/preview.tsx
new file mode 100644
index 0000000..94396c3
--- /dev/null
+++ b/packages/prototypes/src/components/Text/preview.tsx
@@ -0,0 +1,62 @@
+import { createBehavior, createResource } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { defineComponent } from 'vue'
+import { createVoidFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+import './styles.less'
+
+export interface IDesignableTextProps {
+ value?: string
+ content?: string
+ mode?: 'normal' | 'h1' | 'h2' | 'h3' | 'p'
+ style?: Record
+ className?: string
+}
+
+export const Text = composeExport(
+ defineComponent({
+ props: ['mode', 'content'],
+ setup(props, { attrs }) {
+ const TagName = props.mode === 'normal' || !props.mode ? 'div' : props.mode
+ return () => {
+ return (
+
+ {props.content}
+
+ )
+ }
+ }
+ }),
+ {
+ Behavior: createBehavior({
+ name: 'Text',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Text',
+ designerProps: {
+ propsSchema: createVoidFieldSchema(AllSchemas.Text)
+ },
+ designerLocales: AllLocales.Text
+ }),
+
+ Resource: createResource({
+ icon: 'TextSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'string',
+ 'x-component': 'Text'
+ }
+ }
+ ]
+ })
+ }
+)
diff --git a/packages/prototypes/src/components/Text/styles.less b/packages/prototypes/src/components/Text/styles.less
new file mode 100644
index 0000000..b94e412
--- /dev/null
+++ b/packages/prototypes/src/components/Text/styles.less
@@ -0,0 +1,10 @@
+.dn-text {
+ &:empty::before {
+ content: 'Please Input';
+ display: block;
+ opacity: 0.6;
+ }
+ &:focus {
+ padding: 4px;
+ }
+}
diff --git a/packages/prototypes/src/components/TimePicker/index.ts b/packages/prototypes/src/components/TimePicker/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/TimePicker/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/TimePicker/preview.tsx b/packages/prototypes/src/components/TimePicker/preview.tsx
new file mode 100644
index 0000000..7cc662f
--- /dev/null
+++ b/packages/prototypes/src/components/TimePicker/preview.tsx
@@ -0,0 +1,32 @@
+import { TimePicker as FormilyTimePicker } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const TimePicker = composeExport(FormilyTimePicker, {
+ Behavior: createBehavior({
+ name: 'TimePicker',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'TimePicker',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.TimePicker)
+ },
+ designerLocales: AllLocales.TimePicker
+ }),
+ Resource: createResource({
+ icon: 'TimePickerSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'string',
+ title: 'TimePicker',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'TimePicker'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Transfer/index.ts b/packages/prototypes/src/components/Transfer/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Transfer/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Transfer/preview.tsx b/packages/prototypes/src/components/Transfer/preview.tsx
new file mode 100644
index 0000000..9d60215
--- /dev/null
+++ b/packages/prototypes/src/components/Transfer/preview.tsx
@@ -0,0 +1,45 @@
+import { Transfer as FormilyTransfer } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Transfer = composeExport(FormilyTransfer, {
+ Behavior: createBehavior({
+ name: 'Transfer',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Transfer',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Transfer)
+ },
+ designerLocales: AllLocales.Transfer
+ }),
+ Resource: createResource({
+ icon: 'TransferSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ title: 'Transfer',
+ enum: [
+ {
+ key: '1',
+ title: '项目1',
+ description: '描述项目1',
+ disabled: false
+ },
+ {
+ key: '2',
+ title: '项目2',
+ description: '描述项目2',
+ disabled: true
+ }
+ ],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Transfer'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/TreeSelect/index.ts b/packages/prototypes/src/components/TreeSelect/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/TreeSelect/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/TreeSelect/preview.tsx b/packages/prototypes/src/components/TreeSelect/preview.tsx
new file mode 100644
index 0000000..bab0ed6
--- /dev/null
+++ b/packages/prototypes/src/components/TreeSelect/preview.tsx
@@ -0,0 +1,63 @@
+import { TreeSelect as FormilyTreeSelect } from '@formily/antdv'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const TreeSelect = composeExport(FormilyTreeSelect, {
+ Behavior: createBehavior({
+ name: 'TreeSelect',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'TreeSelect',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.TreeSelect)
+ },
+ designerLocales: AllLocales.TreeSelect
+ }),
+ Resource: createResource({
+ icon: 'TreeSelectSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ title: 'TreeSelect',
+ enum: [
+ {
+ value: 1,
+ label: '项目1',
+ children: [
+ {
+ value: 11,
+ label: '项目1-1',
+ disableCheckbox: true,
+ selectable: true
+ },
+ {
+ value: 12,
+ label: '项目1-2'
+ }
+ ]
+ },
+ {
+ value: 2,
+ label: '项目2',
+ children: [
+ {
+ value: 21,
+ label: '项目2-1'
+ },
+ {
+ value: 12,
+ label: '项目2-2'
+ }
+ ]
+ }
+ ],
+ 'x-decorator': 'FormItem',
+ 'x-component': 'TreeSelect'
+ }
+ }
+ ]
+ })
+})
diff --git a/packages/prototypes/src/components/Upload/index.ts b/packages/prototypes/src/components/Upload/index.ts
new file mode 100644
index 0000000..2d3d80b
--- /dev/null
+++ b/packages/prototypes/src/components/Upload/index.ts
@@ -0,0 +1 @@
+export * from './preview'
diff --git a/packages/prototypes/src/components/Upload/preview.tsx b/packages/prototypes/src/components/Upload/preview.tsx
new file mode 100644
index 0000000..4af9901
--- /dev/null
+++ b/packages/prototypes/src/components/Upload/preview.tsx
@@ -0,0 +1,36 @@
+import { Upload as FormilyUpload } from '@formily/antdv'
+import { composeExport } from '@formily/antdv/esm/__builtins__'
+import { createBehavior, createResource } from '@pind/designable-core'
+import { createFieldSchema } from '../Field'
+import { AllSchemas } from '../../schemas'
+import { AllLocales } from '../../locales'
+
+export const Upload = composeExport(FormilyUpload, {
+ Behavior: createBehavior({
+ name: 'Upload',
+ extends: ['Field'],
+ selector: (node) => node.props['x-component'] === 'Upload',
+ designerProps: {
+ propsSchema: createFieldSchema(AllSchemas.Upload)
+ },
+ designerLocales: AllLocales.Upload
+ }),
+
+ Resource: createResource({
+ icon: 'UploadSource',
+ elements: [
+ {
+ componentName: 'Field',
+ props: {
+ type: 'Array