feat(components): add tabbar component

pull/15/head
bqy_fe 3 years ago
parent 32ccceb338
commit 7764fcafc9
  1. 18
      README.EN.md
  2. 2
      README.md
  3. 8
      package.json
  4. 10
      preview/views/preview.vue
  5. 4
      src/packages/base-widgets/datetimePicker/index.tsx
  6. 40
      src/packages/base-widgets/nav-bar/index.tsx
  7. 6
      src/packages/base-widgets/picker/index.tsx
  8. 7
      src/packages/base-widgets/swipe/index.tsx
  9. 116
      src/packages/base-widgets/tabbar/index.tsx
  10. 47
      src/packages/base-widgets/tabbar/tabbar-item.tsx
  11. 6
      src/packages/container-component/form/index.tsx
  12. 6
      src/packages/container-component/layout/index.tsx
  13. 32
      src/shims-vue.d.ts
  14. 4
      src/visual-editor/components/header/preview.vue
  15. 1
      src/visual-editor/components/left-aside/components/base-widgets/index.module.scss
  16. 3
      src/visual-editor/components/left-aside/components/base-widgets/index.tsx
  17. 32
      src/visual-editor/components/left-aside/components/container-component/index.module.scss
  18. 3
      src/visual-editor/components/left-aside/components/container-component/index.tsx
  19. 6
      src/visual-editor/components/left-aside/components/data-source/data-fetch.vue
  20. 238
      src/visual-editor/components/right-attribute-panel/components/attr-editor/AttrEditor.tsx
  21. 172
      src/visual-editor/components/right-attribute-panel/components/attr-editor/components/cross-sortable-options-editor/cross-sortable-options-editor.tsx
  22. 134
      src/visual-editor/components/right-attribute-panel/components/attr-editor/components/prop-config/index.tsx
  23. 138
      src/visual-editor/components/right-attribute-panel/components/attr-editor/index.tsx
  24. 16
      src/visual-editor/components/right-attribute-panel/components/event-action/index.tsx
  25. 37
      src/visual-editor/components/right-attribute-panel/components/form-rule/index.tsx
  26. 4
      src/visual-editor/components/right-attribute-panel/components/index.ts
  27. 5
      src/visual-editor/components/simulator-editor/draggable-transition-group.vue
  28. 34
      src/visual-editor/components/simulator-editor/simulator-editor.vue
  29. 33
      src/visual-editor/components/simulator-editor/slot-item.vue
  30. 34
      src/visual-editor/types/index.d.ts
  31. 26
      src/visual-editor/visual-editor.props.tsx
  32. 35
      src/visual-editor/visual-editor.utils.ts
  33. 2
      vite.config.ts
  34. 31
      yarn.lock

@ -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] ??= {}

32
src/shims-vue.d.ts vendored

@ -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:
<ElInput
v-model={element.label}
class={'my-12px mx-3px'}
style={{ width: '108px' }}
></ElInput>
value:
<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.value}
class={'my-12px mx-3px'}
style={{ width: '106px' }}
v-model={state.list[index]}
class={'m-12px'}
style={{ width: '270px' }}
></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[]
type RequestIdleCallbackHandle = any
type RequestIdleCallbackDeadline = {
readonly didTimeout: boolean
timeRemaining: () => number
}
declare type LabelValueOptions = OptionItem[]
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']
},

@ -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…
Cancel
Save