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 { |
|||
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,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: 卜启缘 |
|||
* @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> |
|||
</> |
|||
) |
|||
} |
|||
}) |
|||
|
|||
Loading…
Reference in new issue