diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1184c67 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 240 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/src/components/Model3DView.vue b/src/components/Model3DView.vue index e5b7ecf..110e9db 100644 --- a/src/components/Model3DView.vue +++ b/src/components/Model3DView.vue @@ -17,6 +17,7 @@ 添加输送线 添加货架 + 添加地堆
材质颜色 @@ -378,6 +379,15 @@ function createShelf(){//创建货架 }) } +function createGroundStore() { + const planeGeometry = new THREE.PlaneGeometry(1, 1); + const material = new THREE.MeshBasicMaterial({ + color: 0x00ff00, + side: THREE.DoubleSide // 双面渲染:ml-citation{ref="5,8" data="citationList"} + }); + const planeMesh = new THREE.Mesh(planeGeometry, material); + scene.add(planeMesh); +} function initThree() { const viewerDom = canvasContainer.value diff --git a/src/components/data-form/DataForm.vue b/src/components/data-form/DataForm.vue new file mode 100644 index 0000000..6e3bf3a --- /dev/null +++ b/src/components/data-form/DataForm.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/src/components/data-form/DataFormConstant.ts b/src/components/data-form/DataFormConstant.ts new file mode 100644 index 0000000..0387773 --- /dev/null +++ b/src/components/data-form/DataFormConstant.ts @@ -0,0 +1,69 @@ +import { markRaw } from "vue"; +import { + ElAutocomplete, + ElCascader, + ElCheckbox, + ElColorPicker, + ElDatePicker, + ElInput, + ElInputNumber, + ElInputTag, + ElMention, + ElRadio, + ElRate, + ElSelect, + ElSelectV2, + ElSlider, + ElSwitch, + ElTimePicker, + ElTimeSelect, + ElTransfer, + ElTreeSelect, + ElUpload, +} from "element-plus"; +import type { DisplayMode } from "./DataFormTypes.ts"; + +/** 内建的表单输入组件 */ +const builtInInputComponents = { + Autocomplete: markRaw(ElAutocomplete), + Cascader: markRaw(ElCascader), + Checkbox: markRaw(ElCheckbox), + ColorPicker: markRaw(ElColorPicker), + DatePicker: markRaw(ElDatePicker), + Input: markRaw(ElInput), + InputNumber: markRaw(ElInputNumber), + InputTag: markRaw(ElInputTag), + Mention: markRaw(ElMention), + Radio: markRaw(ElRadio), + Rate: markRaw(ElRate), + Select: markRaw(ElSelect), + SelectV2: markRaw(ElSelectV2), + Slider: markRaw(ElSlider), + Switch: markRaw(ElSwitch), + TimePicker: markRaw(ElTimePicker), + TimeSelect: markRaw(ElTimeSelect), + Transfer: markRaw(ElTransfer), + TreeSelect: markRaw(ElTreeSelect), + Upload: markRaw(ElUpload), +}; + +type BuiltInInput = keyof typeof builtInInputComponents; + +const dataFormInputComponents: Record = markRaw(builtInInputComponents); + +/** DisplayMode 的默认值 */ +const defDisplayMode: DisplayMode = {}; + +export type { + BuiltInInput, +}; + +export default { + dataFormInputComponents, + defDisplayMode, +} + +export { + dataFormInputComponents, + defDisplayMode, +} diff --git a/src/components/data-form/DataFormTypes.ts b/src/components/data-form/DataFormTypes.ts new file mode 100644 index 0000000..6c96a5e --- /dev/null +++ b/src/components/data-form/DataFormTypes.ts @@ -0,0 +1,276 @@ +import { type ComponentInternalInstance, type CSSProperties } from "vue"; +import { type Breakpoints, breakpointsTailwind } from "@vueuse/core"; +import { type FormItemProps, type FormProps } from "element-plus"; + +/** 表单数据类型 */ +type FormData = Record | Array; + +/** 字段标题 */ +type Label = VueNode | ((value: any, dataPath: string, formData?: FormData) => VueNode); + +/** 表单数据绑定到表单组件上时的格式化操作 */ +type ValueFormat = DictGroup | ((value: any, dataPath: string, formData?: FormData) => any); + +/** 表单组件值更新到表单数据时的数据转换 */ +type ValueTransform = DictGroup | ((value: any, dataPath: string, formData?: FormData) => any); + +/** 前缀、后缀的自定义组件配置 */ +interface PrefixSuffixComponent { + /** 禁用 */ + disabled?: boolean; + /** 组件名或组件对象 */ + component?: any; + /** 组件props */ + props?: Record; + /** 自定义渲染逻辑 */ + render?: (value: any) => VueNode; +} + +/** 显示模式 */ +interface DisplayMode { + /** 禁用 */ + disabled?: boolean; + /** 定义样式 */ + style?: CSSProperties; + /** 自定义class样式 */ + class?: string; + /** 数据格式显示 */ + format?: (value: any) => string; + /** 自定义渲染逻辑 */ + render?: (value: any) => VueNode; +} + +/** 表单字段输入组件通用的props */ +interface FormInputBaseProps { + /** 定义样式 */ + style?: CSSProperties; + /** 自定义class样式 */ + class?: string; + /** 占位符 */ + placeholder?: string; + /** 允许清空 */ + allowClear?: boolean; + /** 只读 */ + readonly?: boolean; + /** 禁用 */ + disabled?: boolean; + /** 隐藏 */ + hidden?: boolean; + /** 前缀区域的自定义组件 */ + prefixConfig?: PrefixSuffixComponent; + /** 后缀区域的自定义组件 */ + suffixConfig?: PrefixSuffixComponent; + /** 前缀区域已经后缀区域的包装容器的样式 */ + preSufWrapStyle?: CSSProperties; + /** 显示模式 */ + displayMode?: DisplayMode | true; + + // onFocus + // onBlur + // onChange + // onPressEnter + + [key: string]: any; +} + +/** 监听表单字段值 */ +interface WatchFormFieldValue { + /** 数据路径,如: "userName"、"user.age"、"user.hobby.[1].name"、"[2].id"*/ + dataPath: string + /** + * 表单字段值变化时的回调 + * @param oldValue 监听的表单字段旧值 + * @param newValue 监听的表单字段新值 + * @param data 整个表单数据 + * @param formItem 表单字段配置(修改当前对象的属性实现ui更新) + * @param input 输入组件实例 + * @param from Form组件实例 + * @param ctxData 表单上下文数据 + */ + onChange: ( + oldValue: V, + newValue: V, + formData: any, + formItem: Pick, + input: any, + from: any, + ctxData: DataFormData["ctxData"], + ) => void + /** 在开始监听时立即触发回调 */ + immediate?: boolean +} + +/** 表单字段,一般包含一个 label 和一个 input */ +interface FormField { + // 核心配置 + /** 数据路径,如: "userName"、"user.age"、"user.hobby.[1].name"、"[2].id" */ + dataPath?: string; + /** 宽度占用列数 */ + widthCount?: number; + /** 字段标题 */ + label?: Label; + /** 字段输入组件,组件名或者组件对象 */ + input?: any; + /** 字段输入组件props */ + inputProps?: FormInputBaseProps; + /** 表单数据绑定到表单组件上时的格式化操作 */ + format?: ValueFormat; + /** 表单组件值更新到表单数据时的数据转换 */ + transform?: ValueTransform; + /** 当字段输入组件有多个时,扩展的输入组件 */ + extInputs?: Array>; + /** 是否隐藏当前字段 */ + hidden?: boolean; + /** 监听表单字段值变化,修改当前表单字段状态 */ + watchValues?: WatchFormFieldValue | Array; + // 扩展配置 + /** 定义样式 */ + style?: CSSProperties; + /** 自定义class样式 */ + class?: string; + /** label宽度 */ + labelWidth?: string | number; + /** label标签的位置 */ + labelPosition?: "left" | "right" + /** 是否显示校验错误信息 */ + showMessage?: boolean; + /** 是否在行内显示校验信息 */ + inlineMessage?: string | boolean; + /** 表单验证规则 */ + rules?: FormItemProps["rules"]; + /** 是否必填,如不设置,则会根据校验规则自动生成 */ + required?: boolean; + /** 原始的 Form.Item 属性 */ + rawProps?: FormItemProps; +} + +/** 运行时的表单项 */ +interface DataFormItem { + /** 表单项占用列数 */ + widthCount: number; + /** 输入组件ref */ + inputRef: string; + /** 输入组件 */ + input?: VueComponent; + /** 输入组件props */ + inputProps?: FormInputBaseProps; + /** 扩展的输入组件 */ + extInputs?: Array>; + /** 需要使用的 FormItem 属性 */ + formItemProps: Record; + /** 数据路径,如: "userName"、"user.age"、"user.hobby.[1].name"、"[2].id" */ + dataPath?: string; + /** 表单数据绑定到表单组件上时的格式化操作 */ + format?: ValueFormat; + /** 表单组件值更新到表单数据时的数据转换 */ + transform?: ValueTransform; + /** 是否隐藏当前表单项 */ + hidden?: boolean; + /** 监听表单字段值变化 */ + watchValues?: Array; +} + +// 定义 Props 类型 +interface DataFormProps { + // 核心配置 + /** 表单名称,会作为表单字段 id 前缀使用 */ + name?: string; + /** 表单数据 */ + data?: FormData; + /** 服务端数据API */ + dataApi?: HttpRequestConfig; + /** 自动使用"dataApi"配置加载服务端数据 */ + autoLoadData?: boolean; + /** 表单字段 */ + formFields?: Array; + /** 响应式断点配置 */ + breakpoints?: Breakpoints; + /** 默认一行显示的字段数量 */ + defColumnCount?: number; + /** 一行显示的字段数量,支持响应式断点配置,最大值:24 */ + columnCount?: number | Record<(keyof typeof breakpointsTailwind) | string, number>; + /** 表单布局:bothFixed:label和input都固定宽度;onlyLabelFixed:仅label固定宽度 */ + layout?: "bothFixed" | "onlyLabelFixed"; + /** label标签的位置 */ + labelPosition?: "left" | "right"; + /** 表单域标签的后缀 */ + labelSuffix?: string; + /** label宽度 */ + labelWidth?: string | number; + /** value宽度,仅layout=bothFixed时有效 */ + inputWidth?: string | number; + /** submit 区域 FormItem 的 Props 配置 */ + submitFormItemProps?: Record; + // 扩展配置 + /** 表单字段大小 */ + size?: "" | "small" | "default" | "large"; + /** 是否显示校验错误信息 */ + showMessage?: boolean; + /** 是否在行内显示校验信息 */ + inlineMessage?: boolean; + /** 是否显示校验错误信息 */ + requireAsteriskPosition?: "left" | "right"; + /** 隐藏所有表单项的必选标记 */ + hideRequiredAsterisk?: boolean; + /** 表单验证规则 */ + rules?: FormProps["rules"]; + /** 是否在 rules 属性改变后立即触发一次验证 */ + validateOnRuleChange?: boolean; + /** 设置表单组件禁用 */ + disabled?: boolean; + /** 当校验失败时,滚动到第一个错误表单项 */ + scrollToError?: boolean; + /** 当校验有失败结果时,滚动到第一个失败的表单项目 可通过 scrollIntoView 配置 */ + scrollIntoViewOptions?: FormProps["scrollIntoViewOptions"]; + /** 注册内置使用的组件 */ + components?: Record; + /** 原始的 Form 属性 */ + rawProps?: FormProps; +} + +// 定义 State 类型 +interface DataFormState { + /** 是否是加载中 */ + loading: boolean; + /** 数据值 */ + data?: FormData; + /** 表格列数 */ + columnCount: number; + /** 表单项数组 */ + dataFormItems: Array; +} + +// 定义 Data 类型 +interface DataFormData { + /** 表单数据是否是第一次变化 */ + firstDataChange: boolean; + /** 表单旧数据,Record */ + oldData: Record; + /** 表单组件的引用 */ + formRef: any; + /** 输入组件的引用 */ + inputRefs: Record; + /** 表单上下文数据 */ + ctxData: { + /** 当前 DataForm 组件实例 */ + instance?: ComponentInternalInstance; + /** 其它属性 */ + [key: string]: any; + }; +} + +export type { + FormData, + Label, + ValueFormat, + ValueTransform, + PrefixSuffixComponent, + DisplayMode, + FormInputBaseProps, + WatchFormFieldValue, + FormField, + DataFormItem, + DataFormProps, + DataFormState, + DataFormData, +} diff --git a/src/components/data-form/DataFormUtils.tsx b/src/components/data-form/DataFormUtils.tsx new file mode 100644 index 0000000..184c658 --- /dev/null +++ b/src/components/data-form/DataFormUtils.tsx @@ -0,0 +1,37 @@ +import lodash from "lodash"; +import { Typeof } from "@ease-forge/shared"; + +/** + * 将字符串转换成 antd 支持的 NamePath + * @param dataPath 数据路径,如: "userName"、"user.age"、"user.hobby.[1].name"、"[2].id" + */ +function dataPathToNamePath(dataPath: string): string | number | Array { + dataPath = lodash.trim(dataPath); + const paths = dataPath.split("."); + const namePath: Array = []; + for (let path of paths) { + path = lodash.trim(path); + let name: string | number = path; + if (path.startsWith("[") && path.endsWith("]")) { + name = lodash.toInteger(path.substring(1, path.length - 1)); + if (!Typeof.isValidNumber(name)) { + name = path; + } + } + namePath.push(name); + } + if (namePath.length === 0) { + return dataPath; + } else if (namePath.length === 1) { + return namePath[0]; + } + return namePath; +} + +export default { + dataPathToNamePath, +} + +export { + dataPathToNamePath, +} diff --git a/src/core/base/IMeta.ts b/src/core/base/IMeta.ts index 680d003..356693f 100644 --- a/src/core/base/IMeta.ts +++ b/src/core/base/IMeta.ts @@ -44,6 +44,7 @@ export type IMeta = MetaItem[] export interface MetaItem { field?: string; editor: string; + editorProps?: any; label?: string; readonly?: boolean; category?: string; diff --git a/src/editor/widgets/property/PropertyView.vue b/src/editor/widgets/property/PropertyView.vue index dd9ad6b..4ea9053 100644 --- a/src/editor/widgets/property/PropertyView.vue +++ b/src/editor/widgets/property/PropertyView.vue @@ -11,36 +11,48 @@
- +
- \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 0a0335c..6828fac 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,29 +1,35 @@ import { createRouter, createWebHashHistory } from 'vue-router' const router = createRouter({ - history: createWebHashHistory(import.meta.env.BASE_URL), - routes: [ - { - path: '/', - name: 'home', - // 自动引导到 /editor - redirect: '/editor' - }, - { - path: '/editor', - name: 'editor', - // component: HomeView, - component: () => import('../editor/ModelMain.vue') - }, - { - path: '/about', - name: 'about', - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import('../views/AboutView.vue') - } - ] + history: createWebHashHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + // 自动引导到 /editor + redirect: '/editor' + }, + { + path: '/editor', + name: 'editor', + // component: HomeView, + component: () => import('../editor/ModelMain.vue') + }, + { + path: '/about', + name: 'about', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import('../views/AboutView.vue') + }, + + { + path: '/DataForm01', + name: 'DataForm01', + component: () => import('@/pages/DataForm01.vue'), + }, + ] }) export default router diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts new file mode 100644 index 0000000..00597a1 --- /dev/null +++ b/src/utils/Utils.ts @@ -0,0 +1,227 @@ +import lodash from "lodash"; +import { createTextVNode, createVNode, Fragment, isVNode, onMounted, onUnmounted, onUpdated, toRaw, type VNode } from "vue"; +import { Format, Typeof } from "@ease-forge/shared"; +import { type AnyFunction, type BaseProps } from "@ease-forge/runtime"; + +interface CreateVNode { + /** + * @param type 组件对象或html标签 + * @param props 组件属性 + * @param children 组件内容或者插槽 + */ + (type: any, props?: BaseProps, children?: Array | Record>): VNode; +} + +/** createVNode函数 */ +const h: CreateVNode = createVNode as any; + +/** + * 调试组件渲染次数 + */ +function debugRender(tag: string = "", ...params: any[]) { + if (APP_INFO.mode === "production") return; + if (tag) tag = `[${tag}]: `; + const nowTime = () => Format.numberFormat(new Date().getTime() / 1000.0, "0.000"); + onMounted(() => console.log(`${nowTime()} | ${tag}加载 onMounted`, ...params)); + onUpdated(() => console.log(`${nowTime()} | ${tag}渲染 onUpdated`, ...params)); + onUnmounted(() => console.log(`${nowTime()} | ${tag}卸载 onUnmounted`, ...params)); +} + +/** + * 把对象装换成 VNode,返回结果可以直接使用 渲染 + */ +function toVNode(vueNode: VueNode, key?: any) { + let props: any = null; + if (Typeof.hasValue(key)) { + props = {}; + props.key = lodash.toString(key); + } + if (isVNode(vueNode)) { + // VNode + return vueNode; + } else if (Typeof.noValue(vueNode)) { + // 空值 + return createTextVNode(); + } else if (Typeof.isArray(vueNode)) { + // 数组 + const children = vueNode.map((node, idx) => toVNode(node, `idx_${idx}`)); + return createVNode(Fragment, props, children); + } else if (Typeof.isFunction(vueNode)) { + // 函数 + return toVNode(vueNode(), key); + } else if (Typeof.isBool(vueNode)) { + // 布尔值 + return createTextVNode(lodash.toString(vueNode)); + } else if (!Typeof.isObj(vueNode)) { + // 非对象 + return createTextVNode(lodash.toString(vueNode)); + } else { + // 其它值 + return createVNode(Fragment, props, [vueNode]); + } +} + +interface MergePropsObjOptions { + /** 表示空值 */ + nullVal: any; + /** 表示未定义的值 */ + unSetVal: any; +} + +const defMergePropsObjOptions: MergePropsObjOptions = { + nullVal: "none", + unSetVal: "unset", +}; + +/** + * 合并组件的 props 属性配置 + * @param propName prop 属性名 + * @param props props 对象 + * @param res 合并 props 配置的结果对象 + * @param options 合并的选项配置 + */ +function mergePropsObj(propName: keyof Props, props: Props, res: any, options?: Partial) { + const ops: MergePropsObjOptions = lodash.defaultsDeep({}, options, defMergePropsObjOptions); + const propValue = props[propName]; + // props 未配置 + if (Typeof.noValue(propValue) || propValue === ops.unSetVal) return; + // props 配置空值 + if (propValue === ops.nullVal) { + res[propName] = undefined; + return; + } + // 如果是对象就合并配置,否则直接覆盖 + if (Typeof.isPlainObj(propValue) && !Typeof.isArray(propValue) && !Typeof.isDate(propValue)) { + res[propName] = lodash.defaultsDeep({}, propValue, res[propName]); + } else { + res[propName] = propValue; + } +} + +/** + * 合并对象指定的函数属性的函数逻辑与当前提供的函数逻辑 + * @param obj 目标操作对象 + * @param funName 函数名 + * @param fun 需要合并的函数逻辑 + */ +function mergeFunction(obj: T, funName: K, fun: T[K]) { + if (!Typeof.isFun(fun)) return; + const rawFun = obj[funName] as Function; + if (!rawFun) { + obj[funName] = fun; + return; + } + // const func:Function = fun; + if (Typeof.isFunction(rawFun)) { + obj[funName] = (function (this: any, ...args: any[]) { + if (Typeof.isAsyncFunction(fun)) { + fun.apply(this, args).then(() => rawFun.apply(this, args)); + } else { + fun.apply(this, args); + return rawFun.apply(this, args); + } + }) as any; + } else if (Typeof.isAsyncFunction(rawFun)) { + obj[funName] = (async function (this: any, ...args: any[]) { + if (Typeof.isAsyncFunction(fun)) { + await fun.apply(this, args); + } else { + fun.apply(this, args); + } + return await rawFun.apply(this, args); + }) as any; + } else { + console.warn("mergeFunction 失败不支持的函数类型:", rawFun); + } +} + +/** + * 合并vue组件的expose对象,会将sources中存在且expose中不存在的属性合并到expose对象 + * @param expose + * @param sources + */ +function mergeExpose(expose: Record, sources: any) { + if (!expose) return; + sources = toRaw(sources); + if (!sources || Typeof.isDate(sources) || Typeof.isArray(sources) || !Typeof.isObj(sources)) return; + // sources 是 dom 对象 + if (sources instanceof HTMLElement || (sources.constructor?.name?.includes("HTML") && sources.constructor?.name?.includes("Element"))) { + expose.$el = sources; + return; + } + // 压制警告[Vue warn] Object.keys(sources) + const rawConsoleWarn = console.warn; + console.warn = _emptyFun; + let keys = Object.keys(sources); + console.warn = rawConsoleWarn; + // 处理 expose + for (let key of keys) { + const newValue = sources[key]; + if (Typeof.noValue(newValue)) continue; + expose[key] = newValue; + } + // 处理 vue 组件内置属性 + keys = ["$data", "$props", "$attrs", "$slots", "$refs", "$emit", "$on", "$off", "$once", "$forceUpdate", "$nextTick", "$watch", "$el", "$options", "$parent", "$root"]; + for (let key of keys) { + const newValue = sources[key]; + if (Typeof.noValue(newValue)) continue; + expose[key] = newValue; + } +} + +// 空函数 +function _emptyFun() { +} + +/** + * 计算响应式断点值 + * @param breakpointsConfig 响应式断点配置 + * @param current 当前响应式断点: useBreakpoints().current() + * @param defValue 默认值 + */ +function calcBreakpointValue(breakpointsConfig: number | Record | undefined, current: Array | undefined, defValue: number): number { + if (Typeof.noValue(breakpointsConfig)) return defValue; + if (Typeof.isNum(breakpointsConfig)) return breakpointsConfig; + let minValue: number | undefined = undefined; + for (let key in breakpointsConfig) { + const value = breakpointsConfig[key]; + if (minValue === undefined) { + minValue = value; + } else if (value < minValue) { + minValue = value; + } + } + if (minValue === undefined) return defValue; + if (!current || current.length <= 0) return minValue; + const breakpoint = current[current.length - 1]; + if (!breakpoint) return defValue; + return breakpointsConfig?.[breakpoint] ?? defValue; +} + +export type { + MergePropsObjOptions, +} + +export default { + createVNode: h, + h, + debugRender, + toVNode, + defMergePropsObjOptions, + mergePropsObj, + mergeFunction, + mergeExpose, + calcBreakpointValue, +} + +export { + createVNode, + h, + debugRender, + toVNode, + defMergePropsObjOptions, + mergePropsObj, + mergeFunction, + mergeExpose, + calcBreakpointValue, +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 975e14a..deca5c2 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,6 +4,7 @@ "module": "esnext", "moduleResolution": "node", "jsx": "preserve", + "jsxImportSource": "vue", "strict": false, "allowJs": true, "checkJs": true, @@ -22,13 +23,8 @@ "baseUrl": ".", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "lib": [ - "es2018", - "es2017", - "es2016", - "es2015.promise", - "dom", - "scripthost", - "es5" + "DOM", + "ESNext" ], "rootDirs": [ "./src" @@ -46,6 +42,7 @@ "include": [ "env.d.ts", "src/**/*.ts", + "src/**/*.tsx", "src/**/*.vue", "types/*.d.ts" ], diff --git a/types/basics.d.ts b/types/basics.d.ts new file mode 100644 index 0000000..512f79b --- /dev/null +++ b/types/basics.d.ts @@ -0,0 +1,14 @@ +import type { Component, DefineComponent, VNode } from 'vue' + +declare global { + /** vue node 原子类型 */ + type VueNodeAtom = VNode | string | number | boolean | Date | null | undefined | (() => VueNode) + + /** vue node 类型,可使用 ComponentsUtils.toVNode(VueNode) 转换成 VNode 后直接渲染 */ + type VueNode = VueNodeAtom | Array + + /** vue组件 */ + type VueComponent = Component | DefineComponent +} + +export {}