34 changed files with 822 additions and 468 deletions
@ -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,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 |
|
||||
} |
|
||||
@ -1,43 +1,45 @@ |
|||||
.list-group { |
|
||||
|
|
||||
} |
|
||||
.list-group-item { |
.list-group-item { |
||||
position: relative; |
position: relative; |
||||
|
display: flex; |
||||
width: calc(100% - 20px); |
width: calc(100% - 20px); |
||||
|
min-height: 120px; |
||||
|
padding: 0 5px; |
||||
|
margin-top: 20px; |
||||
margin-left: 10px; |
margin-left: 10px; |
||||
border: solid 3px #ebeef5; |
border: solid 3px #ebeef5; |
||||
margin-top: 20px; |
transform: translate(0); |
||||
min-height: 120px; |
box-sizing: border-box; |
||||
display: flex; |
|
||||
align-items: center; |
align-items: center; |
||||
justify-content: center; |
justify-content: center; |
||||
padding: 0px 5px; |
|
||||
box-sizing: border-box; |
|
||||
&:hover { |
&:hover { |
||||
border-color: #409EFF; |
|
||||
cursor: move; |
cursor: move; |
||||
|
border-color: #409eff; |
||||
} |
} |
||||
|
|
||||
&:last-of-type { |
&:last-of-type { |
||||
margin-bottom: 20px; |
margin-bottom: 20px; |
||||
} |
} |
||||
|
|
||||
&::before { |
&::before { |
||||
content: attr(data-label); |
|
||||
position: absolute; |
position: absolute; |
||||
top: -3px; |
top: -3px; |
||||
left: -3px; |
left: -3px; |
||||
background-color: #409EFF; |
z-index: 1; |
||||
color: white; |
|
||||
padding: 4px 8px; |
padding: 4px 8px; |
||||
font-size: 12px; |
font-size: 12px; |
||||
z-index: 1; |
color: white; |
||||
|
background-color: #409eff; |
||||
|
content: attr(data-label); |
||||
} |
} |
||||
|
|
||||
&::after { |
&::after { |
||||
content: ""; |
|
||||
position: absolute; |
position: absolute; |
||||
top: 0; |
top: 0; |
||||
left: 0; |
|
||||
right: 0; |
right: 0; |
||||
bottom: 0; |
bottom: 0; |
||||
|
left: 0; |
||||
z-index: 2; |
z-index: 2; |
||||
|
content: ''; |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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 /> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
}) |
|
||||
@ -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,15 +1,48 @@ |
|||||
/* |
/* |
||||
* @Author: 卜启缘 |
* @Author: 卜启缘 |
||||
* @Date: 2021-07-05 10:51:09 |
* @Date: 2021-07-05 10:51:09 |
||||
* @LastEditTime: 2021-07-05 10:52:26 |
* @LastEditTime: 2021-07-08 23:20:17 |
||||
* @LastEditors: 卜启缘 |
* @LastEditors: 卜启缘 |
||||
* @Description: 表单规则 |
* @Description: 表单规则 |
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\form-rule\index.tsx |
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\form-rule\index.tsx |
||||
*/ |
*/ |
||||
import { defineComponent } from 'vue' |
import { defineComponent } from 'vue' |
||||
|
import { ElCard, ElTooltip } from 'element-plus' |
||||
|
|
||||
export const FormRule = defineComponent({ |
export const FormRule = defineComponent({ |
||||
setup() { |
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> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
}) |
}) |
||||
|
|||||
Loading…
Reference in new issue