feat(components): ✨add tabbar component
This commit is contained in:
parent
32ccceb338
commit
7764fcafc9
18
README.EN.md
18
README.EN.md
|
@ -8,8 +8,6 @@
|
|||
|
||||
```shell
|
||||
git clone --single-branch https://github.com/buqiyuan/vite-vue3-lowcode.git
|
||||
# or
|
||||
git clone --single-branch https://gitee.com/buqiyuan/vite-vue3-lowcode.git
|
||||
```
|
||||
|
||||
## technology stack
|
||||
|
@ -67,6 +65,22 @@ JSON.stringify(
|
|||
).replaceAll('"', '')
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 在vant文档中 chrome控制台输入以下代码,快速生成组件事件
|
||||
JSON.stringify(
|
||||
$$('#events + table tbody tr').reduce((prev, curr) => {
|
||||
const children = curr.children
|
||||
const event = {
|
||||
label: children[1].textContent,
|
||||
value: children[0].textContent
|
||||
}
|
||||
return prev.concat([event])
|
||||
}, [])
|
||||
)
|
||||
.replaceAll(/(?<!:)\"(?!,|})/g, '')
|
||||
.replace(/\"/g, "'")
|
||||
```
|
||||
|
||||
## Browser support
|
||||
|
||||
The `Chrome 80+` browser is recommended for local development
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
|
||||
```shell
|
||||
git clone --single-branch https://github.com/buqiyuan/vite-vue3-lowcode.git
|
||||
# or
|
||||
git clone --single-branch https://gitee.com/buqiyuan/vite-vue3-lowcode.git
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vant/touch-emulator": "^1.3.1",
|
||||
"@vant/touch-emulator": "^1.3.2",
|
||||
"@vueuse/core": "^5.1.3",
|
||||
"@vueuse/integrations": "^5.1.3",
|
||||
"animate.css": "^4.1.1",
|
||||
|
@ -34,7 +34,7 @@
|
|||
"nprogress": "^1.0.0-1",
|
||||
"qrcode": "^1.4.4",
|
||||
"qs": "^6.10.1",
|
||||
"vant": "3.1.2",
|
||||
"vant": "3.1.3",
|
||||
"vue": "3.1.4",
|
||||
"vue-router": "^4.0.10",
|
||||
"vuedraggable": "^4.0.3",
|
||||
|
@ -43,7 +43,7 @@
|
|||
"devDependencies": {
|
||||
"@commitlint/cli": "^12.1.4",
|
||||
"@commitlint/config-conventional": "^12.1.4",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/node": "^16.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"@vitejs/plugin-legacy": "^1.4.3",
|
||||
|
@ -64,7 +64,7 @@
|
|||
"lint-staged": "^11.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"sass": "1.35.1",
|
||||
"sass": "1.35.2",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-01 09:45:21
|
||||
* @LastEditTime: 2021-07-04 17:21:47
|
||||
* @LastEditTime: 2021-07-12 10:22:26
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description:
|
||||
* @FilePath: \vite-vue3-lowcode\preview\views\preview.vue
|
||||
|
@ -57,14 +57,16 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
const { bgImage, bgColor } = currentPage.config
|
||||
const bodyStyleStr = `
|
||||
if (currentPage?.config) {
|
||||
const { bgImage, bgColor } = currentPage.config
|
||||
const bodyStyleStr = `
|
||||
body {
|
||||
background-color: ${bgColor};
|
||||
background-image: url(${bgImage});
|
||||
}
|
||||
`
|
||||
document.styleSheets[0].insertRule(bodyStyleStr)
|
||||
document.styleSheets[0].insertRule(bodyStyleStr)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
createEditorSelectProp,
|
||||
createEditorSwitchProp
|
||||
} from '@/visual-editor/visual-editor.props'
|
||||
import { getCurrentInstance, reactive } from 'vue'
|
||||
import { useAttrs, reactive } from 'vue'
|
||||
import { isDate } from '@/visual-editor/utils/is'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
|
@ -29,7 +29,7 @@ export default {
|
|||
render: ({ styles, block, props }) => {
|
||||
const { registerRef } = useGlobalProperties()
|
||||
|
||||
const { attrs } = getCurrentInstance()!
|
||||
const attrs = useAttrs()
|
||||
|
||||
const state = reactive({
|
||||
showPicker: false,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-05-04 05:36:58
|
||||
* @LastEditTime: 2021-07-07 10:56:56
|
||||
* @LastEditTime: 2021-07-11 16:36:05
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 导航栏
|
||||
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\nav-bar\index.tsx
|
||||
|
@ -19,23 +19,37 @@ export default {
|
|||
preview: () => (
|
||||
<NavBar title="标题" left-text="返回" right-text="按钮" left-arrow style={{ width: '100%' }} />
|
||||
),
|
||||
render: ({ props, styles, block, custom }) => {
|
||||
render: ({ props, block }) => {
|
||||
const { registerRef } = useGlobalProperties()
|
||||
|
||||
return (
|
||||
<div style={styles}>
|
||||
<NavBar ref={(el) => registerRef(el, block._vid)} placeholder {...custom} {...props} />
|
||||
</div>
|
||||
)
|
||||
setTimeout(() => {
|
||||
const compEl = window.$$refs[block._vid]?.$el
|
||||
const draggableEl = compEl?.closest('div[data-draggable]')
|
||||
const navbarEl = draggableEl?.querySelector('.van-nav-bar--fixed') as HTMLDivElement
|
||||
if (draggableEl && navbarEl) {
|
||||
navbarEl.style.position = 'unset'
|
||||
draggableEl.style.top = '0'
|
||||
draggableEl.style.left = '0'
|
||||
draggableEl.style.width = '100%'
|
||||
} else {
|
||||
const slotEl = compEl?.closest('__slot-item')
|
||||
if (slotEl) {
|
||||
slotEl.style.position = 'fixed'
|
||||
slotEl.style.bottom = '0'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return <NavBar ref={(el) => registerRef(el, block._vid)} placeholder {...props} />
|
||||
},
|
||||
props: {
|
||||
title: createEditorInputProp({ label: '标题', defaultValue: '标题' }),
|
||||
fixed: createEditorSwitchProp({ label: '是否固定', defaultValue: true }),
|
||||
placeholder: createEditorSwitchProp({
|
||||
label: '是否生成占位元素',
|
||||
defaultValue: true,
|
||||
tips: '固定在顶部时,是否在标签位置生成一个等高的占位元素'
|
||||
}),
|
||||
// placeholder: createEditorSwitchProp({
|
||||
// label: '是否生成占位元素',
|
||||
// defaultValue: true,
|
||||
// tips: '固定在顶部时,是否在标签位置生成一个等高的占位元素'
|
||||
// }),
|
||||
zIndex: createEditorInputProp({ label: 'z-index' }),
|
||||
border: createEditorSwitchProp({ label: '是否显示下边框', defaultValue: false }),
|
||||
leftText: createEditorInputProp({ label: '左侧文案', defaultValue: '返回' }),
|
||||
|
@ -46,6 +60,8 @@ export default {
|
|||
{ label: '点击左侧按钮时触发', value: 'click-left' },
|
||||
{ label: '点击右侧按钮时触发', value: 'click-right' }
|
||||
],
|
||||
showStyleConfig: false,
|
||||
draggable: false,
|
||||
resize: {
|
||||
width: true
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-01 09:45:21
|
||||
* @LastEditTime: 2021-07-07 10:57:41
|
||||
* @LastEditTime: 2021-07-08 15:15:52
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 表单项类型 - 选择器
|
||||
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\picker\index.tsx
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
createEditorInputProp,
|
||||
createEditorModelBindProp
|
||||
} from '@/visual-editor/visual-editor.props'
|
||||
import { reactive, getCurrentInstance } from 'vue'
|
||||
import { reactive, useAttrs } from 'vue'
|
||||
|
||||
export default {
|
||||
key: 'picker',
|
||||
|
@ -25,7 +25,7 @@ export default {
|
|||
render: ({ styles, block, props }) => {
|
||||
const { registerRef } = useGlobalProperties()
|
||||
|
||||
const { attrs } = getCurrentInstance()!
|
||||
const attrs = useAttrs()
|
||||
|
||||
const state = reactive({
|
||||
showPicker: false,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-14 12:24:12
|
||||
* @LastEditTime: 2021-07-07 11:01:14
|
||||
* @LastEditTime: 2021-07-11 16:43:31
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 轮播图组件
|
||||
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\swipe\index.tsx
|
||||
|
@ -29,11 +29,11 @@ export default {
|
|||
<SwipeItem style={swipeItemStyle}>4</SwipeItem>
|
||||
</Swipe>
|
||||
),
|
||||
render: ({ block, props, styles }) => {
|
||||
render: ({ block, props }) => {
|
||||
const { registerRef } = useGlobalProperties()
|
||||
|
||||
return (
|
||||
<div style={styles}>
|
||||
<div>
|
||||
<Swipe
|
||||
ref={(el) => registerRef(el, block._vid)}
|
||||
{...props}
|
||||
|
@ -52,6 +52,7 @@ export default {
|
|||
},
|
||||
props: createFieldProps(),
|
||||
events: [{ label: '每一页轮播结束后触发', value: 'change' }],
|
||||
showStyleConfig: false,
|
||||
resize: {
|
||||
width: true
|
||||
},
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-05-04 05:36:58
|
||||
* @LastEditTime: 2021-07-11 22:38:54
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 导航栏
|
||||
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\tabbar\index.tsx
|
||||
*/
|
||||
import { Tabbar, TabbarItem } from 'vant'
|
||||
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
|
||||
import {
|
||||
createEditorCrossSortableProp,
|
||||
createEditorInputProp,
|
||||
createEditorSwitchProp,
|
||||
createEditorColorProp
|
||||
} from '@/visual-editor/visual-editor.props'
|
||||
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
|
||||
import tabbarItem from './tabbar-item'
|
||||
import { createNewBlock } from '@/visual-editor/visual-editor.utils'
|
||||
import { BASE_URL } from '@/visual-editor/utils'
|
||||
|
||||
export default {
|
||||
key: 'tabbar',
|
||||
moduleName: 'baseWidgets',
|
||||
label: '底部标签栏',
|
||||
preview: () => (
|
||||
<Tabbar>
|
||||
<TabbarItem icon="home-o">首页</TabbarItem>
|
||||
<TabbarItem icon="apps-o">导航</TabbarItem>
|
||||
<TabbarItem icon="user-o">我的</TabbarItem>
|
||||
</Tabbar>
|
||||
),
|
||||
render: ({ props, block }) => {
|
||||
const { registerRef } = useGlobalProperties()
|
||||
|
||||
setTimeout(() => {
|
||||
const compEl = window.$$refs[block._vid]?.$el
|
||||
const draggableEl = compEl?.closest('div[data-draggable]')
|
||||
const tabbarEl = draggableEl?.querySelector('.van-tabbar') as HTMLDivElement
|
||||
if (draggableEl && tabbarEl) {
|
||||
tabbarEl.style.position = 'unset'
|
||||
draggableEl.style.position = 'fixed'
|
||||
draggableEl.style.bottom = '0'
|
||||
draggableEl.style.left = '0'
|
||||
draggableEl.style.width = '100%'
|
||||
draggableEl.style.zIndex = '1000'
|
||||
} else {
|
||||
document.body.style.paddingBottom = '50px'
|
||||
const slotEl = compEl?.closest('__slot-item')
|
||||
if (slotEl) {
|
||||
slotEl.style.position = 'fixed'
|
||||
slotEl.style.bottom = '0'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Tabbar ref={(el) => registerRef(el, block._vid)} v-model={props.modelValue} {...props}>
|
||||
{props.tabs?.map((item) => {
|
||||
const itemProps = item.block?.props
|
||||
const url = `${BASE_URL}${props.baseUrl}${itemProps.url}`.replace(/\/{2,}/g, '/')
|
||||
return (
|
||||
<TabbarItem name={item.value} key={item.value} {...itemProps} url={url}>
|
||||
{item.label}
|
||||
</TabbarItem>
|
||||
)
|
||||
})}
|
||||
</Tabbar>
|
||||
)
|
||||
},
|
||||
props: {
|
||||
modelValue: createEditorInputProp({
|
||||
label: '当前选中标签的名称或索引值',
|
||||
defaultValue: ''
|
||||
}),
|
||||
tabs: createEditorCrossSortableProp({
|
||||
label: '默认选项',
|
||||
labelPosition: 'top',
|
||||
multiple: false,
|
||||
showItemPropsConfig: true,
|
||||
defaultValue: [
|
||||
{ label: '首页', value: 'index', component: tabbarItem, block: createNewBlock(tabbarItem) },
|
||||
{
|
||||
label: '导航',
|
||||
value: 'navigation',
|
||||
component: tabbarItem,
|
||||
block: createNewBlock(tabbarItem)
|
||||
},
|
||||
{ label: '我的', value: 'user', component: tabbarItem, block: createNewBlock(tabbarItem) }
|
||||
]
|
||||
}),
|
||||
fixed: createEditorSwitchProp({ label: '是否固定在底部', defaultValue: true }),
|
||||
border: createEditorSwitchProp({ label: '是否显示外边框', defaultValue: true }),
|
||||
zIndex: createEditorInputProp({ label: '元素 z-index', defaultValue: '1' }),
|
||||
baseUrl: createEditorInputProp({ label: '路由路径前缀', defaultValue: '/preview/#/' }),
|
||||
activeColor: createEditorColorProp({ label: '选中标签的颜色', defaultValue: '#1989fa' }),
|
||||
inactiveColor: createEditorColorProp({ label: '未选中标签的颜色', defaultValue: '#7d7e80' }),
|
||||
route: createEditorSwitchProp({ label: '是否开启路由模式', defaultValue: false }),
|
||||
// placeholder: createEditorSwitchProp({
|
||||
// label: '固定在底部时,是否在标签位置生成一个等高的占位元素',
|
||||
// defaultValue: true
|
||||
// }),
|
||||
safeAreaInsetBottom: createEditorSwitchProp({
|
||||
label: '是否开启底部安全区适配,设置 fixed 时默认开启',
|
||||
defaultValue: false
|
||||
})
|
||||
},
|
||||
events: [
|
||||
{ label: '点击左侧按钮时触发', value: 'click-left' },
|
||||
{ label: '点击右侧按钮时触发', value: 'click-right' }
|
||||
],
|
||||
draggable: false,
|
||||
resize: {
|
||||
width: true
|
||||
}
|
||||
} as VisualEditorComponent
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-05-04 05:36:58
|
||||
* @LastEditTime: 2021-07-11 19:58:14
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 导航栏项
|
||||
* @FilePath: \vite-vue3-lowcode\src\packages\container-component\tabbar\tabbar-item.tsx
|
||||
*/
|
||||
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
|
||||
import { createEditorInputProp, createEditorSwitchProp } from '@/visual-editor/visual-editor.props'
|
||||
|
||||
export default {
|
||||
key: 'tabbar-item',
|
||||
moduleName: 'baseWidgets',
|
||||
label: '底部标签栏',
|
||||
preview: () => <></>,
|
||||
render: () => <></>,
|
||||
props: {
|
||||
// name: createEditorInputProp({
|
||||
// label: '标签名称,作为匹配的标识符',
|
||||
// defaultValue: '当前标签的索引值'
|
||||
// }),
|
||||
icon: createEditorInputProp({ label: '图标名称或图片链接', defaultValue: 'home-o' }),
|
||||
iconPrefix: createEditorInputProp({
|
||||
label: '图标类名前缀',
|
||||
tips: '图标类名前缀,同 Icon 组件的 class-prefix 属性',
|
||||
defaultValue: 'van-icon'
|
||||
}),
|
||||
dot: createEditorSwitchProp({ label: '是否显示图标右上角小红点', defaultValue: false }),
|
||||
badge: createEditorInputProp({ label: '图标右上角徽标的内容', defaultValue: '' }),
|
||||
url: createEditorInputProp({ label: '点击后跳转的链接地址', defaultValue: '' }),
|
||||
// to: createEditorInputProp({
|
||||
// label: '点击后跳转的目标路由对象',
|
||||
// tips: '点击后跳转的目标路由对象,同 vue-router 的 to 属性',
|
||||
// defaultValue: ''
|
||||
// }),
|
||||
replace: createEditorSwitchProp({ label: '是否在跳转时替换当前页面历史', defaultValue: false })
|
||||
},
|
||||
events: [
|
||||
{ label: '点击左侧按钮时触发', value: 'click-left' },
|
||||
{ label: '点击右侧按钮时触发', value: 'click-right' }
|
||||
],
|
||||
draggable: false,
|
||||
resize: {
|
||||
width: true
|
||||
}
|
||||
} as VisualEditorComponent
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-01 09:45:21
|
||||
* @LastEditTime: 2021-07-07 21:23:23
|
||||
* @LastEditTime: 2021-07-08 15:13:02
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description:
|
||||
* @FilePath: \vite-vue3-lowcode\src\packages\container-component\form\index.tsx
|
||||
*/
|
||||
import { Form, Field, Button } from 'vant'
|
||||
import { renderSlot, getCurrentInstance } from 'vue'
|
||||
import { renderSlot, useSlots } from 'vue'
|
||||
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
|
||||
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
|
||||
import { compProps } from './compProps'
|
||||
|
@ -28,7 +28,7 @@ export default {
|
|||
</Form>
|
||||
),
|
||||
render: function ({ props, styles, block }) {
|
||||
const { slots } = getCurrentInstance()!
|
||||
const slots = useSlots()
|
||||
const { registerRef } = useGlobalProperties()
|
||||
|
||||
const onSubmit = (values) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Col, Row } from 'vant'
|
||||
import { renderSlot, getCurrentInstance } from 'vue'
|
||||
import { renderSlot, useSlots } from 'vue'
|
||||
import { createEditorInputProp, createEditorSelectProp } from '@/visual-editor/visual-editor.props'
|
||||
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
|
||||
import styleModule from './index.module.scss'
|
||||
|
@ -36,8 +36,8 @@ export default {
|
|||
<Col span="8">span: 8</Col>
|
||||
</Row>
|
||||
),
|
||||
render: function ({ props, styles, block, custom }) {
|
||||
const { slots } = getCurrentInstance()!
|
||||
render: ({ props, styles, block, custom }) => {
|
||||
const slots = useSlots()
|
||||
const { registerRef } = useGlobalProperties()
|
||||
|
||||
slotsTemp[block._vid] ??= {}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
type RequestIdleCallbackHandle = any
|
||||
type RequestIdleCallbackOptions = {
|
||||
timeout: number
|
||||
}
|
||||
type RequestIdleCallbackDeadline = {
|
||||
readonly didTimeout: boolean
|
||||
timeRemaining: () => number
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
$$refs: any
|
||||
requestIdleCallback: (
|
||||
callback: (deadline: RequestIdleCallbackDeadline) => void,
|
||||
opts?: RequestIdleCallbackOptions
|
||||
) => RequestIdleCallbackHandle
|
||||
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void
|
||||
}
|
||||
|
||||
// declare module '*.vue' {
|
||||
// import { DefineComponent } from 'vue'
|
||||
//
|
||||
// const component: DefineComponent<{}, {}, any>
|
||||
// export default component
|
||||
// }
|
||||
|
||||
// declare module '*.module.scss'
|
||||
|
||||
declare module '*.vue' {
|
||||
import { ComponentOptions } from 'vue'
|
||||
const component: ComponentOptions
|
||||
export default component
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<el-dialog v-model="dialogVisible" custom-class="h5-preview" :show-close="false" width="360px">
|
||||
<iframe
|
||||
v-if="dialogVisible"
|
||||
:style="{ width: '360px', height: '640px' }"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
:src="previewUrl"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
|
@ -48,6 +48,8 @@ export default defineComponent({
|
|||
overflow: hidden;
|
||||
|
||||
.el-dialog__body {
|
||||
width: 360px;
|
||||
height: 640px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
margin-top: 20px;
|
||||
margin-left: 10px;
|
||||
border: solid 3px #ebeef5;
|
||||
transform: translate(0);
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-01 13:22:14
|
||||
* @LastEditTime: 2021-07-06 20:32:39
|
||||
* @LastEditTime: 2021-07-11 11:05:06
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 基础组件
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\base-widgets\index.tsx
|
||||
|
@ -28,7 +28,6 @@ export default defineComponent({
|
|||
const cloneDog = (comp) => {
|
||||
console.log('当前拖拽的组件:', comp)
|
||||
const newComp = cloneDeep(comp)
|
||||
newComp._vid = Date.now()
|
||||
return createNewBlock(newComp)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,43 +1,45 @@
|
|||
.list-group {
|
||||
|
||||
}
|
||||
.list-group-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: calc(100% - 20px);
|
||||
min-height: 120px;
|
||||
padding: 0 5px;
|
||||
margin-top: 20px;
|
||||
margin-left: 10px;
|
||||
border: solid 3px #ebeef5;
|
||||
margin-top: 20px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
transform: translate(0);
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0px 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
cursor: move;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
background-color: #409EFF;
|
||||
color: white;
|
||||
z-index: 1;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
background-color: #409eff;
|
||||
content: attr(data-label);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-01 13:22:14
|
||||
* @LastEditTime: 2021-07-04 21:36:46
|
||||
* @LastEditTime: 2021-07-11 11:04:06
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description:
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\container-component\index.tsx
|
||||
|
@ -27,7 +27,6 @@ export default defineComponent({
|
|||
const cloneDog = (comp) => {
|
||||
console.log('当前拖拽的组件:', comp)
|
||||
const newComp = cloneDeep(comp)
|
||||
newComp._vid = Date.now()
|
||||
return createNewBlock(newComp)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-24 18:36:03
|
||||
* @LastEditTime: 2021-07-07 21:48:03
|
||||
* @LastEditTime: 2021-07-09 20:00:22
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 接口请求
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\data-fetch.vue
|
||||
|
@ -167,6 +167,8 @@ const showModelMoal = () => {
|
|||
</ElFormItem>
|
||||
<ElFormItem label="请求数据" prop={'data.bind'}>
|
||||
<ElCascader
|
||||
v-model={state.ruleForm.data.bind}
|
||||
options={models.value}
|
||||
clearable={true}
|
||||
props={{
|
||||
checkStrictly: true,
|
||||
|
@ -177,8 +179,6 @@ const showModelMoal = () => {
|
|||
}}
|
||||
placeholder="请选择绑定的请求数据"
|
||||
onChange={handleBindChange}
|
||||
v-model={state.ruleForm.data.bind}
|
||||
options={models.value}
|
||||
></ElCascader>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="响应数据" prop={'data.recv'}>
|
||||
|
|
|
@ -1,238 +0,0 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-10 16:23:06
|
||||
* @LastEditTime: 2021-07-07 19:36:45
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 组件属性编辑器
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\attr-editor\AttrEditor.tsx
|
||||
*/
|
||||
import { defineComponent, computed, watch } from 'vue'
|
||||
import {
|
||||
ElColorPicker,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElPopover,
|
||||
ElCascader,
|
||||
ElInputNumber,
|
||||
ElRadioGroup,
|
||||
ElRadioButton
|
||||
} from 'element-plus'
|
||||
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props'
|
||||
import { TablePropEditor, CrossSortableOptionsEditor } from './components'
|
||||
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
|
||||
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { FormatInputNumber } from '@/visual-editor/components/common/format-input-number'
|
||||
|
||||
export const AttrEditor = defineComponent({
|
||||
setup() {
|
||||
const { visualConfig, currentBlock, jsonData } = useVisualData()
|
||||
/**
|
||||
* @description 模型集合
|
||||
*/
|
||||
const models = computed(() => cloneDeep(jsonData.models))
|
||||
|
||||
const compPaddingAttrs = ['paddingTop', 'paddingLeft', 'paddingRight', 'paddingBottom']
|
||||
|
||||
/**
|
||||
* @description 监听组件padding值的变化
|
||||
*/
|
||||
watch(
|
||||
compPaddingAttrs.map((item) => () => currentBlock.value.styles?.[item]),
|
||||
(val: string[]) => {
|
||||
const isSame = val.every((item) => currentBlock.value.styles?.tempPadding == item)
|
||||
if (isSame || new Set(val).size === 1) {
|
||||
if (Reflect.has(currentBlock.value, 'styles')) {
|
||||
currentBlock.value.styles.tempPadding = val[0]
|
||||
}
|
||||
} else {
|
||||
currentBlock.value.styles.tempPadding = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* @description 总的组件padding变化时进行的操作
|
||||
*/
|
||||
const compPadding = computed({
|
||||
get: () => currentBlock.value.styles?.tempPadding,
|
||||
set(val) {
|
||||
compPaddingAttrs.forEach((item) => (currentBlock.value.styles[item] = val))
|
||||
currentBlock.value.styles.tempPadding = val
|
||||
}
|
||||
})
|
||||
|
||||
const renderEditor = (propName: string, propConfig: VisualEditorProps) => {
|
||||
const { propObj, prop } = useDotProp(currentBlock.value.props, propName)
|
||||
|
||||
propObj[prop] ??= propConfig.defaultValue
|
||||
|
||||
return {
|
||||
[VisualEditorPropsType.input]: () => {
|
||||
if (!Object.is(propObj[prop], undefined) && !Object.is(propObj[prop], null)) {
|
||||
propObj[prop] = `${propObj[prop]}`
|
||||
}
|
||||
return (
|
||||
<ElInput v-model={propObj[prop]} placeholder={propConfig.tips || propConfig.label} />
|
||||
)
|
||||
},
|
||||
[VisualEditorPropsType.inputNumber]: () => <ElInputNumber v-model={propObj[prop]} />,
|
||||
[VisualEditorPropsType.switch]: () => <ElSwitch v-model={propObj[prop]} />,
|
||||
[VisualEditorPropsType.color]: () => <ElColorPicker v-model={propObj[prop]} />,
|
||||
[VisualEditorPropsType.crossSortable]: () => (
|
||||
<CrossSortableOptionsEditor v-model={propObj[prop]} multiple={propConfig.multiple} />
|
||||
),
|
||||
[VisualEditorPropsType.select]: () => (
|
||||
<ElSelect v-model={propObj[prop]} valueKey={'value'} multiple={propConfig.multiple}>
|
||||
{propConfig.options?.map((opt) => (
|
||||
<ElOption label={opt.label} style={{ fontFamily: opt.value }} value={opt.value} />
|
||||
))}
|
||||
</ElSelect>
|
||||
),
|
||||
[VisualEditorPropsType.table]: () => (
|
||||
<TablePropEditor v-model={propObj[prop]} propConfig={propConfig} />
|
||||
),
|
||||
[VisualEditorPropsType.modelBind]: () => (
|
||||
<ElCascader
|
||||
clearable={true}
|
||||
props={{
|
||||
checkStrictly: true,
|
||||
children: 'entitys',
|
||||
label: 'name',
|
||||
value: 'key',
|
||||
expandTrigger: 'hover'
|
||||
}}
|
||||
placeholder="请选择绑定的请求数据"
|
||||
v-model={propObj[prop]}
|
||||
options={models.value}
|
||||
></ElCascader>
|
||||
)
|
||||
}[propConfig.type]()
|
||||
}
|
||||
|
||||
// 表单项
|
||||
const FormEditor = () => {
|
||||
const content: JSX.Element[] = []
|
||||
if (currentBlock.value) {
|
||||
const { componentKey } = currentBlock.value
|
||||
const component = visualConfig.componentMap[componentKey]
|
||||
console.log('props.block:', currentBlock.value)
|
||||
content.push(
|
||||
<>
|
||||
<ElFormItem label="组件ID" labelWidth={'76px'}>
|
||||
{currentBlock.value._vid}
|
||||
<ElPopover
|
||||
width={200}
|
||||
trigger="hover"
|
||||
content={`你可以利用该组件ID。对该组件进行获取和设置其属性,组件可用属性可在控制台输入:$$refs.${currentBlock.value._vid} 进行查看`}
|
||||
>
|
||||
{{
|
||||
reference: () => <i class={'el-icon-warning-outline ml-6px'}></i>
|
||||
}}
|
||||
</ElPopover>
|
||||
</ElFormItem>
|
||||
</>
|
||||
)
|
||||
if (!!component) {
|
||||
if (!!component.props) {
|
||||
content.push(
|
||||
...Object.entries(component.props || {}).map(([propName, propConfig]) => (
|
||||
<>
|
||||
<ElFormItem
|
||||
key={currentBlock.value._vid + propName}
|
||||
style={
|
||||
propConfig.labelPosition == 'top'
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{{
|
||||
label: () => (
|
||||
<>
|
||||
{propConfig.tips && (
|
||||
<ElPopover width={200} trigger={'hover'} content={propConfig.tips}>
|
||||
{{
|
||||
reference: () => <i class={'el-icon-warning-outline'}></i>
|
||||
}}
|
||||
</ElPopover>
|
||||
)}
|
||||
{propConfig.label}
|
||||
</>
|
||||
),
|
||||
default: () => renderEditor(propName, propConfig)
|
||||
}}
|
||||
</ElFormItem>
|
||||
</>
|
||||
))
|
||||
)
|
||||
content.push(
|
||||
<ElFormItem label={'组件对齐方式'} labelWidth={'90px'}>
|
||||
<ElRadioGroup v-model={currentBlock.value.styles.justifyContent} size="mini">
|
||||
<ElRadioButton label="flex-start"></ElRadioButton>
|
||||
<ElRadioButton label="center"></ElRadioButton>
|
||||
<ElRadioButton label="flex-end"></ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
)
|
||||
content.push(
|
||||
<>
|
||||
<ElFormItem class={'flex flex-col justify-start'}>
|
||||
{{
|
||||
label: () => (
|
||||
<div class={'flex justify-between mb-2'}>
|
||||
<div>组件内边距</div>
|
||||
<FormatInputNumber v-model={compPadding.value} class={'!w-100px'} />
|
||||
</div>
|
||||
),
|
||||
default: () => (
|
||||
<div class={'grid grid-cols-3 gap-2 w-full bg-gray-100 p-20px items-center'}>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingTop}
|
||||
class={'!w-100px col-span-full col-start-2'}
|
||||
/>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingLeft}
|
||||
class={'!w-100px col-span-1'}
|
||||
/>
|
||||
<div class={'bg-white col-span-1 h-40px'}></div>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingRight}
|
||||
class={'!w-100px col-span-1'}
|
||||
/>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingBottom}
|
||||
class={'!w-100px col-span-full col-start-2'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</ElFormItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ElForm size="mini" labelPosition={'left'}>
|
||||
{content}
|
||||
</ElForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<FormEditor />
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-14 15:00:45
|
||||
* @LastEditTime: 2021-07-03 10:00:59
|
||||
* @LastEditTime: 2021-07-12 10:15:21
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 可以拖拽排序的选项列表
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\attr-editor\components\cross-sortable-options-editor\cross-sortable-options-editor.tsx
|
||||
|
@ -9,18 +9,36 @@
|
|||
|
||||
import { defineComponent, reactive, computed, PropType } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { ElInput, ElCheckboxGroup, ElCheckbox } from 'element-plus'
|
||||
import {
|
||||
ElInput,
|
||||
ElCheckboxGroup,
|
||||
ElCheckbox,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElTabs,
|
||||
ElTabPane,
|
||||
ElForm
|
||||
} from 'element-plus'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { isObject } from '@/visual-editor/utils/is'
|
||||
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
|
||||
import { PropConfig } from '../prop-config'
|
||||
import { VisualEditorBlockData, VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
interface OptionItem extends LabelValue {
|
||||
component?: VisualEditorComponent
|
||||
block?: VisualEditorBlockData
|
||||
}
|
||||
|
||||
export const CrossSortableOptionsEditor = defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<(string | LabelValue)[]>,
|
||||
type: Array as PropType<(string | OptionItem)[]>,
|
||||
default: () => []
|
||||
},
|
||||
multiple: Boolean // 是否多选
|
||||
multiple: Boolean, // 是否多选
|
||||
showItemPropsConfig: Boolean // 是否多选
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { currentBlock } = useVisualData()
|
||||
|
@ -58,80 +76,98 @@ export const CrossSortableOptionsEditor = defineComponent({
|
|||
}
|
||||
|
||||
const incrementOption = (index) => {
|
||||
const length = state.list.length + 1
|
||||
const newItem = state.list.some((item) => isObject(item))
|
||||
? {
|
||||
label: '',
|
||||
value: ''
|
||||
}
|
||||
? Object.assign(cloneDeep(state.list[0]), {
|
||||
label: `选项${length}`,
|
||||
value: `选项${length}`
|
||||
})
|
||||
: ''
|
||||
state.list.splice(index + 1, 0, newItem)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<ElCheckboxGroup
|
||||
modelValue={checkList.value}
|
||||
style={{ fontSize: 'inherit' }}
|
||||
onChange={onChange}
|
||||
>
|
||||
<Draggable
|
||||
tag="ul"
|
||||
list={state.list}
|
||||
class="list-group"
|
||||
component-data={{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !state.drag ? 'flip-list' : null
|
||||
}}
|
||||
handle=".handle"
|
||||
{...dragOptions.value}
|
||||
itemKey={''}
|
||||
onStart={() => (state.drag = true)}
|
||||
onEnd={() => (state.drag = false)}
|
||||
<div>
|
||||
<ElCheckboxGroup
|
||||
modelValue={checkList.value}
|
||||
style={{ fontSize: 'inherit' }}
|
||||
onChange={onChange}
|
||||
>
|
||||
{{
|
||||
item: ({ element, index }) => (
|
||||
<div class={'flex items-center justify-between'}>
|
||||
<i class={'el-icon-rank handle cursor-move'}></i>
|
||||
{isObject(element) ? (
|
||||
<>
|
||||
<ElCheckbox label={element.value} class={'ml-5px'}>
|
||||
{''}
|
||||
</ElCheckbox>
|
||||
label:
|
||||
<Draggable
|
||||
tag="ul"
|
||||
list={state.list}
|
||||
class="list-group"
|
||||
component-data={{
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !state.drag ? 'flip-list' : null
|
||||
}}
|
||||
handle=".handle"
|
||||
{...dragOptions.value}
|
||||
itemKey={''}
|
||||
onStart={() => (state.drag = true)}
|
||||
onEnd={() => (state.drag = false)}
|
||||
>
|
||||
{{
|
||||
item: ({ element, index }) => (
|
||||
<div class={'flex items-center justify-between'}>
|
||||
<i class={'el-icon-rank handle cursor-move'}></i>
|
||||
{isObject(element) ? (
|
||||
<>
|
||||
<ElCheckbox label={element.value} class={'ml-5px'}>
|
||||
{''}
|
||||
</ElCheckbox>
|
||||
label:
|
||||
<ElInput
|
||||
v-model={element.label}
|
||||
class={'my-12px mx-3px'}
|
||||
style={{ width: '108px' }}
|
||||
></ElInput>
|
||||
value:
|
||||
<ElInput
|
||||
v-model={element.value}
|
||||
class={'my-12px mx-3px'}
|
||||
style={{ width: '106px' }}
|
||||
></ElInput>
|
||||
</>
|
||||
) : (
|
||||
<ElInput
|
||||
v-model={element.label}
|
||||
class={'my-12px mx-3px'}
|
||||
style={{ width: '108px' }}
|
||||
v-model={state.list[index]}
|
||||
class={'m-12px'}
|
||||
style={{ width: '270px' }}
|
||||
></ElInput>
|
||||
value:
|
||||
<ElInput
|
||||
v-model={element.value}
|
||||
class={'my-12px mx-3px'}
|
||||
style={{ width: '106px' }}
|
||||
></ElInput>
|
||||
</>
|
||||
) : (
|
||||
<ElInput
|
||||
v-model={state.list[index]}
|
||||
class={'m-12px'}
|
||||
style={{ width: '270px' }}
|
||||
></ElInput>
|
||||
)}
|
||||
<div class={'flex flex-col'}>
|
||||
<i
|
||||
class={'el-icon-circle-plus-outline'}
|
||||
onClick={() => incrementOption(index)}
|
||||
></i>
|
||||
<i
|
||||
class={'el-icon-remove-outline'}
|
||||
onClick={() => state.list.splice(index, 1)}
|
||||
></i>
|
||||
)}
|
||||
<div class={'flex flex-col'}>
|
||||
<i
|
||||
class={'el-icon-circle-plus-outline hover:text-blue-400 cursor-pointer'}
|
||||
onClick={() => incrementOption(index)}
|
||||
></i>
|
||||
<i
|
||||
class={'el-icon-remove-outline hover:text-red-500 cursor-pointer'}
|
||||
onClick={() => state.list.splice(index, 1)}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Draggable>
|
||||
</ElCheckboxGroup>
|
||||
)
|
||||
}}
|
||||
</Draggable>
|
||||
</ElCheckboxGroup>
|
||||
{props.showItemPropsConfig && (
|
||||
<ElCollapse>
|
||||
<ElCollapseItem title={'选项配置'}>
|
||||
<ElTabs type={'border-card'}>
|
||||
{state.list.map((item: OptionItem) => (
|
||||
<ElTabPane label={item.label} key={item.label}>
|
||||
<ElForm size="mini" labelPosition={'left'}>
|
||||
<PropConfig component={item.component} block={item.block} />
|
||||
</ElForm>
|
||||
</ElTabPane>
|
||||
))}
|
||||
</ElTabs>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-07-11 17:53:54
|
||||
* @LastEditTime: 2021-07-11 18:36:17
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 组件属性配置
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\attr-editor\components\prop-config\index.tsx
|
||||
*/
|
||||
|
||||
import { computed, defineComponent, PropType } from 'vue'
|
||||
import {
|
||||
ElColorPicker,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElCascader,
|
||||
ElInputNumber,
|
||||
ElFormItem,
|
||||
ElPopover
|
||||
} from 'element-plus'
|
||||
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
|
||||
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props'
|
||||
import { TablePropEditor, CrossSortableOptionsEditor } from '../../components'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
|
||||
import { VisualEditorBlockData, VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
|
||||
|
||||
export const PropConfig = defineComponent({
|
||||
props: {
|
||||
component: {
|
||||
type: Object as PropType<VisualEditorComponent>,
|
||||
default: () => ({})
|
||||
},
|
||||
block: {
|
||||
type: Object as PropType<VisualEditorBlockData>,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { jsonData } = useVisualData()
|
||||
/**
|
||||
* @description 模型集合
|
||||
*/
|
||||
const models = computed(() => cloneDeep(jsonData.models))
|
||||
|
||||
const renderPropItem = (propName: string, propConfig: VisualEditorProps) => {
|
||||
const { propObj, prop } = useDotProp(props.block.props, propName)
|
||||
|
||||
propObj[prop] ??= propConfig.defaultValue
|
||||
|
||||
return {
|
||||
[VisualEditorPropsType.input]: () => {
|
||||
if (!Object.is(propObj[prop], undefined) && !Object.is(propObj[prop], null)) {
|
||||
propObj[prop] = `${propObj[prop]}`
|
||||
}
|
||||
return (
|
||||
<ElInput v-model={propObj[prop]} placeholder={propConfig.tips || propConfig.label} />
|
||||
)
|
||||
},
|
||||
[VisualEditorPropsType.inputNumber]: () => <ElInputNumber v-model={propObj[prop]} />,
|
||||
[VisualEditorPropsType.switch]: () => <ElSwitch v-model={propObj[prop]} />,
|
||||
[VisualEditorPropsType.color]: () => <ElColorPicker v-model={propObj[prop]} />,
|
||||
[VisualEditorPropsType.crossSortable]: () => (
|
||||
<CrossSortableOptionsEditor
|
||||
v-model={propObj[prop]}
|
||||
multiple={propConfig.multiple}
|
||||
showItemPropsConfig={propConfig.showItemPropsConfig}
|
||||
/>
|
||||
),
|
||||
[VisualEditorPropsType.select]: () => (
|
||||
<ElSelect v-model={propObj[prop]} valueKey={'value'} multiple={propConfig.multiple}>
|
||||
{propConfig.options?.map((opt) => (
|
||||
<ElOption label={opt.label} style={{ fontFamily: opt.value }} value={opt.value} />
|
||||
))}
|
||||
</ElSelect>
|
||||
),
|
||||
[VisualEditorPropsType.table]: () => (
|
||||
<TablePropEditor v-model={propObj[prop]} propConfig={propConfig} />
|
||||
),
|
||||
[VisualEditorPropsType.modelBind]: () => (
|
||||
<ElCascader
|
||||
clearable={true}
|
||||
props={{
|
||||
checkStrictly: true,
|
||||
children: 'entitys',
|
||||
label: 'name',
|
||||
value: 'key',
|
||||
expandTrigger: 'hover'
|
||||
}}
|
||||
placeholder="请选择绑定的请求数据"
|
||||
v-model={propObj[prop]}
|
||||
options={models.value}
|
||||
></ElCascader>
|
||||
)
|
||||
}[propConfig.type]()
|
||||
}
|
||||
|
||||
return () => {
|
||||
return Object.entries(props.component.props ?? {}).map(([propName, propConfig]) => (
|
||||
<>
|
||||
<ElFormItem
|
||||
key={props.block._vid + propName}
|
||||
style={
|
||||
propConfig.labelPosition == 'top'
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{{
|
||||
label: () => (
|
||||
<>
|
||||
{propConfig.tips && (
|
||||
<ElPopover width={200} trigger={'hover'} content={propConfig.tips}>
|
||||
{{
|
||||
reference: () => <i class={'el-icon-warning-outline'}></i>
|
||||
}}
|
||||
</ElPopover>
|
||||
)}
|
||||
{propConfig.label}
|
||||
</>
|
||||
),
|
||||
default: () => renderPropItem(propName, propConfig)
|
||||
}}
|
||||
</ElFormItem>
|
||||
</>
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-10 16:23:06
|
||||
* @LastEditTime: 2021-07-11 18:36:24
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 组件属性编辑器
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\attr-editor\index.tsx
|
||||
*/
|
||||
import { defineComponent, computed, watch } from 'vue'
|
||||
import { ElForm, ElFormItem, ElPopover, ElRadioGroup, ElRadioButton } from 'element-plus'
|
||||
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
|
||||
import { FormatInputNumber } from '@/visual-editor/components/common/format-input-number'
|
||||
import { PropConfig } from './components/prop-config'
|
||||
|
||||
export const AttrEditor = defineComponent({
|
||||
setup() {
|
||||
const { visualConfig, currentBlock } = useVisualData()
|
||||
|
||||
const compPaddingAttrs = ['paddingTop', 'paddingLeft', 'paddingRight', 'paddingBottom']
|
||||
|
||||
/**
|
||||
* @description 监听组件padding值的变化
|
||||
*/
|
||||
watch(
|
||||
compPaddingAttrs.map((item) => () => currentBlock.value.styles?.[item]),
|
||||
(val: string[]) => {
|
||||
const isSame = val.every((item) => currentBlock.value.styles?.tempPadding == item)
|
||||
if (isSame || new Set(val).size === 1) {
|
||||
if (Reflect.has(currentBlock.value, 'styles')) {
|
||||
currentBlock.value.styles.tempPadding = val[0]
|
||||
}
|
||||
} else {
|
||||
currentBlock.value.styles.tempPadding = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* @description 总的组件padding变化时进行的操作
|
||||
*/
|
||||
const compPadding = computed({
|
||||
get: () => currentBlock.value.styles?.tempPadding,
|
||||
set(val) {
|
||||
compPaddingAttrs.forEach((item) => (currentBlock.value.styles[item] = val))
|
||||
currentBlock.value.styles.tempPadding = val
|
||||
}
|
||||
})
|
||||
|
||||
// 表单项
|
||||
const FormEditor = () => {
|
||||
const content: JSX.Element[] = []
|
||||
if (currentBlock.value) {
|
||||
const { componentKey } = currentBlock.value
|
||||
const component = visualConfig.componentMap[componentKey]
|
||||
console.log('props.block:', currentBlock.value)
|
||||
content.push(
|
||||
<>
|
||||
<ElFormItem label="组件ID" labelWidth={'76px'}>
|
||||
{currentBlock.value._vid}
|
||||
<ElPopover
|
||||
width={200}
|
||||
trigger="hover"
|
||||
content={`你可以利用该组件ID。对该组件进行获取和设置其属性,组件可用属性可在控制台输入:$$refs.${currentBlock.value._vid} 进行查看`}
|
||||
>
|
||||
{{
|
||||
reference: () => <i class={'el-icon-warning-outline ml-6px'}></i>
|
||||
}}
|
||||
</ElPopover>
|
||||
</ElFormItem>
|
||||
</>
|
||||
)
|
||||
if (!!component) {
|
||||
if (!!component.props) {
|
||||
content.push(<PropConfig component={component} block={currentBlock.value} />)
|
||||
{
|
||||
currentBlock.value.showStyleConfig &&
|
||||
content.push(
|
||||
<ElFormItem label={'组件对齐方式'} labelWidth={'90px'}>
|
||||
<ElRadioGroup v-model={currentBlock.value.styles.justifyContent} size="mini">
|
||||
<ElRadioButton label="flex-start">{'左对齐'}</ElRadioButton>
|
||||
<ElRadioButton label="center">{'居中'}</ElRadioButton>
|
||||
<ElRadioButton label="flex-end">{'右对齐'}</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>,
|
||||
<ElFormItem class={'flex flex-col justify-start'}>
|
||||
{{
|
||||
label: () => (
|
||||
<div class={'flex justify-between mb-2'}>
|
||||
<div>组件内边距</div>
|
||||
<FormatInputNumber v-model={compPadding.value} class={'!w-100px'} />
|
||||
</div>
|
||||
),
|
||||
default: () => (
|
||||
<div
|
||||
class={'grid grid-cols-3 gap-2 w-full bg-gray-100 p-20px items-center'}
|
||||
>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingTop}
|
||||
class={'!w-100px col-span-full col-start-2'}
|
||||
/>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingLeft}
|
||||
class={'!w-100px col-span-1'}
|
||||
/>
|
||||
<div class={'bg-white col-span-1 h-40px'}></div>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingRight}
|
||||
class={'!w-100px col-span-1'}
|
||||
/>
|
||||
<FormatInputNumber
|
||||
v-model={currentBlock.value.styles.paddingBottom}
|
||||
class={'!w-100px col-span-full col-start-2'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</ElFormItem>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ElForm size="mini" labelPosition={'left'}>
|
||||
{content}
|
||||
</ElForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<FormEditor />
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
|
@ -1,12 +1,12 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-24 11:01:45
|
||||
* @LastEditTime: 2021-07-07 21:07:07
|
||||
* @LastEditTime: 2021-07-08 09:53:27
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 事件-动作
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\event-action\index.tsx
|
||||
*/
|
||||
import { computed, defineComponent, reactive } from 'vue'
|
||||
import { computed, ref, defineComponent, reactive } from 'vue'
|
||||
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
|
||||
import {
|
||||
ElForm,
|
||||
|
@ -27,7 +27,6 @@ import { useModal } from '@/visual-editor/hooks/useModal'
|
|||
import { cloneDeep } from 'lodash'
|
||||
|
||||
interface IState {
|
||||
ruleFormRef: NonNullable<any>
|
||||
activeNames: string[]
|
||||
ruleForm: Action
|
||||
}
|
||||
|
@ -59,9 +58,9 @@ export const EventAction = defineComponent({
|
|||
const isEdit = computed(() =>
|
||||
currentBlock.value.actions?.some((item) => item.key === state.ruleForm.key)
|
||||
)
|
||||
const ruleFormRef = ref<InstanceType<typeof ElForm>>()
|
||||
|
||||
const state = reactive<IState>({
|
||||
ruleFormRef: null,
|
||||
activeNames: [],
|
||||
ruleForm: createEmptyAction()
|
||||
})
|
||||
|
@ -162,12 +161,7 @@ export const EventAction = defineComponent({
|
|||
title: `${operateType}动作`,
|
||||
props: { width: 600 },
|
||||
content: () => (
|
||||
<ElForm
|
||||
model={state.ruleForm}
|
||||
ref={(el) => el && (state.ruleFormRef = el)}
|
||||
label-width="100px"
|
||||
size={'mini'}
|
||||
>
|
||||
<ElForm model={state.ruleForm} ref={ruleFormRef} label-width="100px" size={'mini'}>
|
||||
<ElFormItem
|
||||
label="事件"
|
||||
prop={'event'}
|
||||
|
@ -247,7 +241,7 @@ export const EventAction = defineComponent({
|
|||
),
|
||||
onConfirm: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
state.ruleFormRef.validate((valid) => {
|
||||
ruleFormRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const index = currentBlock.value.actions.findIndex(
|
||||
(item) => item.key == state.ruleForm.key
|
||||
|
|
|
@ -1,15 +1,48 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-07-05 10:51:09
|
||||
* @LastEditTime: 2021-07-05 10:52:26
|
||||
* @LastEditTime: 2021-07-08 23:20:17
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 表单规则
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\form-rule\index.tsx
|
||||
*/
|
||||
import { defineComponent } from 'vue'
|
||||
import { ElCard, ElTooltip } from 'element-plus'
|
||||
|
||||
export const FormRule = defineComponent({
|
||||
setup() {
|
||||
return () => <>表单规则</>
|
||||
return () => (
|
||||
<>
|
||||
<ElCard shadow={'always'} class={'mb-20px'}>
|
||||
{{
|
||||
header: () => (
|
||||
<div class="flex justify-between">
|
||||
<span>设置关联规则</span>
|
||||
<ElTooltip content="当前面题目选中某些选项时才出现此题" placement="bottom-end">
|
||||
<i class={'el-icon-question'}></i>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
),
|
||||
default: () => <div>暂无规则</div>
|
||||
}}
|
||||
</ElCard>
|
||||
<ElCard shadow={'always'} bodyStyle={{ padding: 1 ? '0' : '20px' }} class={'mb-20px'}>
|
||||
{{
|
||||
header: () => (
|
||||
<div class="flex justify-between">
|
||||
<span>设置选项关联规则</span>
|
||||
<ElTooltip
|
||||
content="当前面题目选择某些选项时才出现此题的某些选项 "
|
||||
placement="bottom-end"
|
||||
>
|
||||
<i class={'el-icon-question'}></i>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
),
|
||||
default: () => null
|
||||
}}
|
||||
</ElCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-12 22:18:48
|
||||
* @LastEditTime: 2021-07-05 10:53:16
|
||||
* @LastEditTime: 2021-07-11 18:05:22
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 统一导出组件
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\index.ts
|
||||
*/
|
||||
|
||||
export { AttrEditor } from './attr-editor/AttrEditor'
|
||||
export { AttrEditor } from './attr-editor'
|
||||
export { Animate } from './animate/Animate'
|
||||
export { PageSetting } from './page-setting/pageSetting'
|
||||
export { EventAction } from './event-action/'
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
name: !isDrag ? 'flip-list' : null
|
||||
}"
|
||||
:group="group"
|
||||
v-bind="{ ...dragOptions, $attrs }"
|
||||
v-bind="{ ...dragOptions, ...$attrs }"
|
||||
:item-key="itemKey"
|
||||
@start="isDrag = true"
|
||||
@end="isDrag = false"
|
||||
>
|
||||
<template #item="item">
|
||||
<div>
|
||||
<div :class="{ 'item-drag': item.element.draggable }" :data-el="item.element.draggable">
|
||||
<slot name="item" v-bind="item"> </slot>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -67,6 +67,7 @@ export default defineComponent({
|
|||
const dragOptions = computed(() => ({
|
||||
animation: 200,
|
||||
disabled: false,
|
||||
scroll: true,
|
||||
ghostClass: 'ghost'
|
||||
}))
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<template>
|
||||
<div class="simulator-container">
|
||||
<div class="simulator-editor">
|
||||
<div class="simulator-editor-content" :style="pageStyle">
|
||||
<div class="simulator-editor-content">
|
||||
<DraggableTransitionGroup
|
||||
v-model:drag="drag"
|
||||
v-model="currentPage.blocks"
|
||||
style="min-height: 500px"
|
||||
class="!min-h-680px"
|
||||
draggable=".item-drag"
|
||||
>
|
||||
<template #item="{ element: outElement }">
|
||||
<div
|
||||
|
@ -51,7 +52,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="tsx">
|
||||
import { defineComponent, reactive, computed, toRefs } from 'vue'
|
||||
import { defineComponent, reactive, watchEffect, toRefs } from 'vue'
|
||||
import type { VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
|
||||
import DraggableTransitionGroup from './draggable-transition-group.vue'
|
||||
import { $$dropdown, DropdownOption } from '@/visual-editor/utils/dropdown-service'
|
||||
|
@ -80,12 +81,23 @@ export default defineComponent({
|
|||
drag: false
|
||||
})
|
||||
|
||||
const pageStyle = computed(() => {
|
||||
/**
|
||||
* @description 操作当前页面样式表
|
||||
*/
|
||||
watchEffect(() => {
|
||||
const { bgImage, bgColor } = currentPage.value.config
|
||||
return {
|
||||
backgroundImage: `url(${bgImage})`,
|
||||
backgroundColor: bgColor
|
||||
const bodyStyleStr = `
|
||||
.simulator-editor-content {
|
||||
background-color: ${bgColor};
|
||||
background-image: url(${bgImage});
|
||||
}`
|
||||
const styleSheets = document.styleSheets[0]
|
||||
const firstCssRule = document.styleSheets[0].cssRules[0]
|
||||
const isExistContent = firstCssRule.cssText.includes('.simulator-editor-content')
|
||||
if (isExistContent) {
|
||||
styleSheets.deleteRule(0)
|
||||
}
|
||||
styleSheets.insertRule(bodyStyleStr)
|
||||
})
|
||||
|
||||
//递归实现
|
||||
|
@ -118,7 +130,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// 给当前点击的组件设置聚焦
|
||||
const handleSlotsFocus = (block, _vid) => {
|
||||
const handleSlotsFocus = (block: VisualEditorBlockData, _vid: string) => {
|
||||
const slots = block.props?.slots || {}
|
||||
if (Object.keys(slots).length > 0) {
|
||||
Object.keys(slots).forEach((key) => {
|
||||
|
@ -138,7 +150,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// 选择要操作的组件
|
||||
const selectComp = (element) => {
|
||||
const selectComp = (element: VisualEditorBlockData) => {
|
||||
setCurrentBlock(element)
|
||||
currentPage.value.blocks.forEach((block) => {
|
||||
block.focus = element._vid == block._vid
|
||||
|
@ -234,7 +246,6 @@ export default defineComponent({
|
|||
return {
|
||||
...toRefs(state),
|
||||
currentPage,
|
||||
pageStyle,
|
||||
deleteComp,
|
||||
selectComp,
|
||||
onContextmenuBlock
|
||||
|
@ -266,7 +277,6 @@ export default defineComponent({
|
|||
overflow: hidden auto;
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
transform: translate(0);
|
||||
box-sizing: border-box;
|
||||
background-clip: content-box;
|
||||
contain: layout;
|
||||
|
@ -277,6 +287,7 @@ export default defineComponent({
|
|||
|
||||
&-content {
|
||||
min-height: 100%;
|
||||
transform: translate(0);
|
||||
box-shadow: 0 8px 12px #ebedf0;
|
||||
}
|
||||
}
|
||||
|
@ -285,7 +296,6 @@ export default defineComponent({
|
|||
position: relative;
|
||||
padding: 3px;
|
||||
cursor: move;
|
||||
transform: translate(0);
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<template>
|
||||
<draggable-transition-group
|
||||
:key="slotKey"
|
||||
v-model="slotChildren"
|
||||
v-model:drag="isDrag"
|
||||
class="inner-draggable"
|
||||
:class="{ slot: !slotChildren?.length }"
|
||||
draggable=".item-drag"
|
||||
:data-slot="`插槽(${slotKey})\n 拖拽组件到此处`"
|
||||
>
|
||||
<template #item="{ element: innerElement }">
|
||||
<div
|
||||
class="list-group-item inner"
|
||||
:data-label="innerElement.label"
|
||||
:class="{ focus: innerElement.focus, focusWithChild: innerElement.focusWithChild }"
|
||||
:class="{
|
||||
focus: innerElement.focus,
|
||||
focusWithChild: innerElement.focusWithChild
|
||||
}"
|
||||
@contextmenu.stop.prevent="onContextmenuBlock($event, innerElement, slotChildren)"
|
||||
@mousedown.stop="selectComp(innerElement)"
|
||||
>
|
||||
|
@ -45,35 +48,48 @@
|
|||
* @update: 2021/5/2 22:36
|
||||
*/
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import DraggableTransitionGroup from './draggable-transition-group.vue'
|
||||
import CompRender from './comp-render'
|
||||
import type { VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SlotItem',
|
||||
components: { CompRender, DraggableTransitionGroup },
|
||||
props: {
|
||||
slotKey: String,
|
||||
slotKey: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
children: {
|
||||
type: Array,
|
||||
type: Array as PropType<VisualEditorBlockData[]>,
|
||||
default: () => []
|
||||
},
|
||||
selectComp: {
|
||||
type: Function,
|
||||
type: Function as PropType<(comp: VisualEditorBlockData) => void>,
|
||||
required: true
|
||||
},
|
||||
onContextmenuBlock: {
|
||||
type: Function,
|
||||
type: Function as PropType<
|
||||
(
|
||||
e: MouseEvent,
|
||||
block: VisualEditorBlockData,
|
||||
parentBlocks?: VisualEditorBlockData[]
|
||||
) => void
|
||||
>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:children', 'on-selected', 'update:drag'],
|
||||
setup(props, { emit }) {
|
||||
// 初始化时设置上次选中的组件
|
||||
props.children.some((item) => item.focus && !void props.selectComp(item))
|
||||
|
||||
return {
|
||||
isDrag: useVModel(props, 'drag', emit),
|
||||
slotChildren: useVModel(props, 'children', emit)
|
||||
|
@ -113,7 +129,6 @@ export default defineComponent({
|
|||
position: relative;
|
||||
padding: 3px;
|
||||
cursor: move;
|
||||
transform: translate(0);
|
||||
|
||||
&.focusWithChild {
|
||||
@include showContainerBorder;
|
||||
|
|
|
@ -1,18 +1,40 @@
|
|||
/*
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-01 09:45:21
|
||||
* @LastEditTime: 2021-07-07 21:45:41
|
||||
* @LastEditTime: 2021-07-08 16:47:34
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 全局声明文件
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\types\index.d.ts
|
||||
*/
|
||||
|
||||
declare type LabelValue = {
|
||||
label: string
|
||||
value: any
|
||||
}
|
||||
declare global {
|
||||
/** label-value一般用做选项 */
|
||||
type LabelValue = {
|
||||
label: string
|
||||
value: any
|
||||
}
|
||||
/** label-value 数组 一般用做选项 */
|
||||
type LabelValueOptions = OptionItem[]
|
||||
|
||||
declare type LabelValueOptions = OptionItem[]
|
||||
type RequestIdleCallbackHandle = any
|
||||
|
||||
type RequestIdleCallbackDeadline = {
|
||||
readonly didTimeout: boolean
|
||||
timeRemaining: () => number
|
||||
}
|
||||
|
||||
type RequestIdleCallbackOptions = {
|
||||
timeout: number
|
||||
}
|
||||
interface Window {
|
||||
$$refs: any
|
||||
requestIdleCallback: (
|
||||
callback: (deadline: RequestIdleCallbackDeadline) => void,
|
||||
opts?: RequestIdleCallbackOptions
|
||||
) => RequestIdleCallbackHandle
|
||||
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface ComponentCustomProperties {
|
||||
|
|
|
@ -1,23 +1,39 @@
|
|||
export enum VisualEditorPropsType {
|
||||
/** 输入框 */
|
||||
input = 'input',
|
||||
/** 数字输入框 */
|
||||
inputNumber = 'InputNumber',
|
||||
/** 颜色选择器 */
|
||||
color = 'color',
|
||||
/** 下拉选择器 */
|
||||
select = 'select',
|
||||
/** 表格 */
|
||||
table = 'table',
|
||||
/** 开关 */
|
||||
switch = 'switch',
|
||||
/** 模型绑定选择器 */
|
||||
modelBind = 'ModelBind',
|
||||
/** 可拖拽项 */
|
||||
crossSortable = 'CrossSortable'
|
||||
}
|
||||
|
||||
export type VisualEditorProps = {
|
||||
type: VisualEditorPropsType
|
||||
/** 表单项标签名称 */
|
||||
label: string
|
||||
tips?: string // 表单项提示
|
||||
labelPosition?: string // 表单域标签的位置
|
||||
multiple?: boolean
|
||||
/** 表单项提示说明 */
|
||||
tips?: string
|
||||
/** 表单域标签的位置 */
|
||||
labelPosition?: string
|
||||
/** 表单项默认值 */
|
||||
defaultValue?: any
|
||||
} & {
|
||||
/** 可选项 */
|
||||
options?: VisualEditorSelectOptions
|
||||
/** 是否可以多选 */
|
||||
multiple?: boolean
|
||||
/** 项属性配置 */
|
||||
showItemPropsConfig?: boolean
|
||||
} & {
|
||||
table?: VisualEditorTableOption
|
||||
}
|
||||
|
@ -123,6 +139,7 @@ export function createEditorColorProp({ label, defaultValue }: EditorColorProp):
|
|||
export type VisualEditorSelectOptions = {
|
||||
label: string
|
||||
value: string | number | boolean | object
|
||||
[prop: string]: any
|
||||
}[]
|
||||
|
||||
interface EditorSelectProp {
|
||||
|
@ -185,6 +202,7 @@ interface EditorCrossSortableProp {
|
|||
label: string
|
||||
labelPosition: 'top' | ''
|
||||
multiple?: boolean
|
||||
showItemPropsConfig?: boolean
|
||||
defaultValue?: string[] | VisualEditorSelectOptions
|
||||
}
|
||||
|
||||
|
@ -192,12 +210,14 @@ export function createEditorCrossSortableProp({
|
|||
label,
|
||||
labelPosition,
|
||||
multiple,
|
||||
showItemPropsConfig,
|
||||
defaultValue
|
||||
}: EditorCrossSortableProp): VisualEditorProps {
|
||||
return {
|
||||
type: VisualEditorPropsType.crossSortable,
|
||||
label,
|
||||
multiple,
|
||||
showItemPropsConfig,
|
||||
labelPosition,
|
||||
defaultValue
|
||||
}
|
||||
|
|
|
@ -28,6 +28,10 @@ export interface VisualEditorBlockData {
|
|||
props: Record<string, any>
|
||||
/** 绑定的字段 */
|
||||
model: Record<string, string>
|
||||
/** 组件是否可以被拖拽 */
|
||||
draggable: boolean
|
||||
/** 是否显示组件样式配置项 */
|
||||
showStyleConfig?: boolean
|
||||
/** 动画集 */
|
||||
animations?: Animation[]
|
||||
/** 组件动作集合 */
|
||||
|
@ -182,11 +186,17 @@ export interface Animation {
|
|||
* @description 单个组件注册规则
|
||||
*/
|
||||
export interface VisualEditorComponent {
|
||||
key: string // 组件名称
|
||||
moduleName: keyof ComponentModules // 模块名称
|
||||
_vid?: string // 组件id 时间戳
|
||||
/** 组件name */
|
||||
key: string
|
||||
/** 组件所属模块名称 */
|
||||
moduleName: keyof ComponentModules
|
||||
/** 组件唯一id */
|
||||
_vid?: string
|
||||
/** 组件中文名称 */
|
||||
label: string
|
||||
/** 组件预览函数 */
|
||||
preview: () => JSX.Element
|
||||
/** 组件渲染函数 */
|
||||
render: (data: {
|
||||
props: any
|
||||
model: any
|
||||
|
@ -194,9 +204,17 @@ export interface VisualEditorComponent {
|
|||
block: VisualEditorBlockData
|
||||
custom: Record<string, any>
|
||||
}) => JSX.Element
|
||||
/** 组件是否可以被拖拽 */
|
||||
draggable?: boolean
|
||||
/** 是否显示组件的样式配置项 */
|
||||
showStyleConfig?: boolean
|
||||
/** 组件属性 */
|
||||
props?: Record<string, VisualEditorProps>
|
||||
animations?: Animation[] // 动画集
|
||||
events?: { label: string; value: string }[] // 组件事件集合
|
||||
/** 动画集 */
|
||||
animations?: Animation[]
|
||||
/** 组件事件集合 */
|
||||
events?: { label: string; value: string }[]
|
||||
/** 组件样式 */
|
||||
styles?: CSSProperties
|
||||
}
|
||||
|
||||
|
@ -206,10 +224,9 @@ export interface VisualEditorMarkLines {
|
|||
}
|
||||
|
||||
export function createNewBlock(component: VisualEditorComponent): VisualEditorBlockData {
|
||||
component._vid = `${component._vid}`.startsWith('vid_') ? component._vid : `vid_${component._vid}`
|
||||
|
||||
const cid = parseInt(`${Date.now() * Math.random()}`)
|
||||
return {
|
||||
_vid: component._vid!,
|
||||
_vid: `vid_${cid}`,
|
||||
moduleName: component.moduleName,
|
||||
componentKey: component!.key,
|
||||
label: component!.label,
|
||||
|
@ -232,6 +249,8 @@ export function createNewBlock(component: VisualEditorComponent): VisualEditorBl
|
|||
}
|
||||
return prev
|
||||
}, {}),
|
||||
draggable: component.draggable ?? true, // 是否可以拖拽
|
||||
showStyleConfig: component.showStyleConfig ?? true, // 是否显示组件样式配置
|
||||
animations: [], // 动画集
|
||||
actions: [], // 动作集合
|
||||
events: component.events || [], // 事件集合
|
||||
|
|
|
@ -84,7 +84,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
|||
'element-plus',
|
||||
'vant',
|
||||
'lodash',
|
||||
'vuedraggable/src/vuedraggable'
|
||||
'vuedraggable'
|
||||
],
|
||||
exclude: ['vue-demi']
|
||||
},
|
||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -681,11 +681,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256"
|
||||
integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
|
||||
|
||||
"@types/node@*", "@types/node@^16.0.0":
|
||||
"@types/node@*":
|
||||
version "16.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.0.0.tgz#067a6c49dc7a5c2412a505628e26902ae967bf6f"
|
||||
integrity sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg==
|
||||
|
||||
"@types/node@^16.3.1":
|
||||
version "16.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.3.1.tgz#24691fa2b0c3ec8c0d34bfcfd495edac5593ebb4"
|
||||
integrity sha512-N87VuQi7HEeRJkhzovao/JviiqKjDKMVKxKMfUvSKw+MbkbW8R0nA3fi/MQhhlxV2fQ+2ReM+/Nt4efdrJx3zA==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||
|
@ -808,10 +813,10 @@
|
|||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
|
||||
"@vant/touch-emulator@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@vant/touch-emulator/-/touch-emulator-1.3.1.tgz#93a0c8824d47482c660b982045378d45245e07a0"
|
||||
integrity sha512-7qid+kyhvFdW7qMhxn0M9ClivvZY1sf2ph5Lu7cSVlA3s+tdtnySwOiAdZc4BSVJQUejeE63XxZIn/p6e45Uvw==
|
||||
"@vant/touch-emulator@^1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@vant/touch-emulator/-/touch-emulator-1.3.2.tgz#9392c3971dd7247c2cf8e5d5aabc951e33cd5e6f"
|
||||
integrity sha512-Om6e8kCAnmk/q8byngKreff7Hyn6XxwOGr8yedP3y3LEVoE+iyj8/+Mn+AYvGEQ00GK0MlgAfyaV4emXAYj1Hw==
|
||||
|
||||
"@vant/use@^1.1.2":
|
||||
version "1.1.2"
|
||||
|
@ -5315,10 +5320,10 @@ safe-buffer@~5.2.0:
|
|||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sass@1.35.1:
|
||||
version "1.35.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.1.tgz#90ecf774dfe68f07b6193077e3b42fb154b9e1cd"
|
||||
integrity sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ==
|
||||
sass@1.35.2:
|
||||
version "1.35.2"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.2.tgz#b732314fcdaf7ef8d0f1698698adc378043cb821"
|
||||
integrity sha512-jhO5KAR+AMxCEwIH3v+4zbB2WB0z67V1X0jbapfVwQQdjHZUGUyukpnoM6+iCMfsIUC016w9OPKQ5jrNOS9uXw==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
|
||||
|
@ -6254,10 +6259,10 @@ validate-npm-package-license@^3.0.1:
|
|||
spdx-correct "^3.0.0"
|
||||
spdx-expression-parse "^3.0.0"
|
||||
|
||||
vant@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vant/-/vant-3.1.2.tgz#8777290f38c4b042d22612343e72dcab95b044ed"
|
||||
integrity sha512-uw+ZKZTD44L2YojenZjRuF/rETR28rnuI7cQ4tFtEJBLKc1TNxsxl3PYmoGbYt7jd5rMrHYST8SkBsbB5i4kNA==
|
||||
vant@3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/vant/-/vant-3.1.3.tgz#c87a1b86bbf0e3c139a519d89e310427fa36d8b3"
|
||||
integrity sha512-TihKiAUE+Kf2Cx0/sokMtk6/WTZWTwHygBwOgWKB2eJ43piMogcRYpgK66UK7EATfn00565Rp+trIkAwE63+zQ==
|
||||
dependencies:
|
||||
"@vant/icons" "^1.6.0"
|
||||
"@vant/lazyload" "^1.2.0"
|
||||
|
|
Loading…
Reference in New Issue