parent
32ccceb338
commit
7764fcafc9
@ -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