属性更新
- 属性编辑通过 store 获取属性值
- 通过发射事件触发 commit 修改属性值
- 支持属性值的转换
import { TextComponentProps } from '@/ts/defaultProps'
// 属性转化成表单 哪个属性使用哪个类型的组件去编辑
export interface PropsToForm {
component: string
// 支持给组件库传入属性
extraProps?: { [key: string]: any }
text: string
// 支持组件包裹
subComponent?: string
// 包裹的组件选项
options?: {
text: string
value: any
}[]
// 支持类型转换
initalTransform?: (v: any) => any
//因为开始定义的是any,这样会各个组件的类型不同发生错误,处理更新后的函数
afterTransform?: (v: any) => any
// 支持自定义属性名称
valueProp?: string
//触发事件的名称
eventName?: string
}
// 属性列表转化成表单列表
export type PropsToForms = {
[p in keyof TextComponentProps]?: PropsToForm
}
// 属性转化成表单的映射表 key:属性 value:使用的组件
export const mapPropsToForms: PropsToForms = {
// 比如: text 属性,使用 a-input 这个组件去编辑
text: {
component: 'a-textarea',
extraProps: {
rows: 3,
},
text: '文本',
afterTransform: (e: any) => e.target.value,
},
fontSize: {
text: '字号',
component: 'a-input-number',
initalTransform: (v: string) => parseInt(v),
afterTransform: (e: any) => (e ? `${e}px` : ''),
},
lineHeight: {
text: '行高',
component: 'a-slider',
extraProps: {
min: 0,
max: 3,
step: 0.1,
},
initalTransform: (v: string) => parseFloat(v),
afterTransform: (v: string) => String(v),
},
textAlign: {
component: 'a-radio-group',
subComponent: 'a-radio-button',
text: '对齐',
options: [
{
value: 'left',
text: '左',
},
{
value: 'center',
text: '中',
},
{
value: 'right',
text: '右',
},
],
afterTransform: (e: any) => e.target.value,
},
fontFamily: {
component: 'a-select',
subComponent: 'a-select-option',
text: '字体',
options: [
{
value: '',
text: '无',
},
{
value: '"SimSun","STSong',
text: '宋体',
},
{
value: '"SimHei","STHeiti',
text: '黑体',
},
],
afterTransform: (e: any) => e,
},
}
<template>
<div class="props-table">
<div v-for="(item, index) in finalPros" class="prop-item" :key="index">
<span class="label" v-if="item.text">{{ item.text }}</span>
<div class="prop-component">
<!--v-bind 绑定属性 v-on 绑定事件 重点-->
<component
v-if="item.valueProp"
:is="item?.component"
:value="item?.valueProp"
v-bind="item.extraProps"
v-on="item.events"
>
<template v-if="item.options">
<component
v-for="(option, k) in item.options"
:is="item.subComponent"
:value="option.value"
:key="k"
>
{{ option.text }}
</component>
</template>
</component>
</div>
</div>
</div>
</template>
<script lang="ts">
import { TextComponentProps } from '@/ts/defaultProps'
import { mapPropsToForms, PropsToForms } from '@/ts/propsMap'
import { reduce } from 'lodash'
import { computed, defineComponent, PropType } from 'vue'
export interface FormProps {
component: string
subComponent?: string
value: string
extraProps?: { [key: string]: any }
text?: string
options?: {
text: string
value: any
}[]
initalTransform?: (v: any) => any
valueProp: string
eventName: string
events: { [key: string]: (e: any) => void }
}
export default defineComponent({
name: 'props-table',
props: {
props: {
type: Object as PropType<TextComponentProps>,
required: true,
},
},
emits: ['change'],
setup(props, context) {
const finalPros = computed(() => {
//重点1
return reduce(
props.props,
(res, value, key) => {
// console.log(value, key) //value是每一个组件属性值 value表示属性
const newKey = key as keyof TextComponentProps
const item = mapPropsToForms[newKey]
if (item) {
const {
valueProp = value,
eventName = 'change',
initalTransform,
afterTransform,
} = item
//转成特定的form表单
const newItem: FormProps = {
...item,
value: initalTransform ? initalTransform(value) : value,
valueProp: initalTransform
? initalTransform(valueProp)
: valueProp,
eventName,
events: {
[eventName]: (e: any) => {
context.emit('change', {
key,
value: afterTransform ? afterTransform(e) : e,
})
},
},
}
res[newKey] = newItem
}
// console.log(res)
return res
},
{} as { [key: string]: FormProps },
)
})
return { finalPros }
},
})
</script>
...
<template>
<div class="editor-container">
...
<a-layout-sider
width="300"
style="background: #fff"
class="settings-panel"
>
组件属性
<props-table
v-if="currentElement && currentElement?.props"
:props="currentElement?.props"
@change="handleChange"
></props-table>
</a-layout-sider>
</a-layout>
</div>
</template>
<script lang="ts">
...
import {
ADDCOMPONENT,
DELCOMPONENT,
AETACTIVE,
GETCURRENTCOMPONENT,
UPDATECOMPONENT,
} from '@/store/mutation-types'
export default defineComponent({
components: { LText, ComponentsList, CloseOutlined, EditWrapper, PropsTable },
setup() {
...
const handleChange = (e: any) => {
console.log('event', e)
store.commit(UPDATECOMPONENT, e)
}
return {
handleChange,
}
},
})
</script>
...
import {
ADDCOMPONENT,
DELCOMPONENT,
AETACTIVE,
GETCURRENTCOMPONENT,
UPDATECOMPONENT,
} from './mutation-types'
...
//两个参数 第一个本地的interface,第二个是全局的interface
const editor: Module<EditorProps, GloabalDataProps> = {
state: {
components: testComponents,
currentElement: '',
},
mutations: {
...
[UPDATECOMPONENT](state, { key, value }) {
const updatedComponent = state.components.find(
(component) => component.id === state.currentElement,
)
if (updatedComponent) {
updatedComponent.props[key as keyof TextComponentProps] = value
}
},
},
...
}
export default editor
优化需求
使用 h 函数改写
...
import { h, VNode } from 'vue';
const fontFamilyArr = [
{ text: '宋体', value: '"SimSun","STSong"' },
{ text: '黑体', value: '"SimHei","STHeiti"' },
{ text: '楷体', value: '"KaiTi","STKaiti"' },
{ text: '仿宋', value: '"FangSong","STFangsong"' },
]
const fontFamilyOptions = fontFamilyArr.map((font) => {
return {
value: font.value,
text: h('span', { style: { fontFamily: font.value } }, font.text),
};
});
// 属性转化成表单的映射表 key:属性 value:使用的组件
export const mapPropsToForms: PropsToForms = {
fontFamily: {
component: 'a-select',
subComponent: 'a-select-option',
text: '字体',
options: [
{
value: '',
text: '无',
},
...fontFamilyOptions,
],
afterTransform: (e: any) => e,
},
};
...
tsx
import { TextComponentProps } from '@/ts/defaultProps'
import { VNode } from 'vue'
// 属性转化成表单 哪个属性使用哪个类型的组件去编辑
export interface PropsToForm {
component: string
// 支持给组件库传入属性
extraProps?: { [key: string]: any }
text?: string
// 支持组件包裹
subComponent?: string
// 包裹的组件选项
options?: {
text: string | VNode
value: any
}[]
// 支持类型转换
initalTransform?: (v: any) => any
afterTransform?: (v: any) => any
// 支持自定义属性名称
valueProp?: string
//触发事件的名称
eventName?: string
}
// 属性列表转化成表单列表
export type PropsToForms = {
[p in keyof TextComponentProps]?: PropsToForm
}
const fontFamilyArr = [
{ text: '宋体', value: '"SimSun","STSong"' },
{ text: '黑体', value: '"SimHei","STHeiti"' },
{ text: '楷体', value: '"KaiTi","STKaiti"' },
{ text: '仿宋', value: '"FangSong","STFangsong"' },
]
const fontFamilyOptions = fontFamilyArr.map((font) => {
return {
value: font.value,
text: (
<span style={{ fontFamily: font.value }}>{font.text}</span>
) as VNode,
}
})
// 属性转化成表单的映射表 key:属性 value:使用的组件
const pxToNumberHandler: PropsToForm = {
component: 'a-input-number',
initalTransform: (v: string) => parseInt(v),
afterTransform: (e: number) => (e ? `${e}px` : ''),
}
export const mapPropsToForms: PropsToForms = {
text: {
text: '文本',
component: 'a-textarea',
extraProps: { rows: 3 },
afterTransform: (e: any) => e.target.value,
},
fontSize: {
text: '字号',
...pxToNumberHandler,
},
lineHeight: {
text: '行高',
component: 'a-slider',
extraProps: { min: 0, max: 3, step: 0.1 },
initalTransform: (v: string) => parseFloat(v),
afterTransform: (e: number) => e.toString(),
},
textAlign: {
component: 'a-radio-group',
subComponent: 'a-radio-button',
text: '对齐',
options: [
{ value: 'left', text: '左' },
{ value: 'center', text: '中' },
{ value: 'right', text: '右' },
],
afterTransform: (e: any) => e.target.value,
},
fontFamily: {
component: 'a-select',
subComponent: 'a-select-option',
text: '字体',
options: fontFamilyOptions,
},
width: {
text: '宽度',
...pxToNumberHandler,
},
}
方案一:使用 JSX 重写 PropsTable 组件(较推荐)
import { computed, defineComponent, PropType, VNode } from 'vue'
import { reduce } from 'lodash'
import { PropsToForms, mapPropsToForms } from '../propsMap'
import { TextComponentProps } from '../defaultProps'
import { Input, InputNumber, Slider, Radio, Select } from 'ant-design-vue'
const mapToComponent = {
'a-textarea': Input.TextArea,
'a-input-number': InputNumber,
'a-slider': Slider,
'a-radio-group': Radio.Group,
'a-radio-button': Radio.Button,
'a-select': Select,
'a-select-option': Select.Option
} as any
interface FormProps {
component: string;
subComponent?: string;
value: string;
extraProps?: { [key: string]: any };
text?: string;
options?: { text: string | VNode; value: any }[];
valueProp: string;
eventName: string;
events: { [key: string]: (e: any) => void };
}
function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
export default defineComponent({
name: 'props-table',
props: {
props: {
type: Object as PropType<TextComponentProps>,
required: true
}
},
emits: ['change'],
setup(props, context) {
const finalProps = computed(() => {
return reduce(props.props, (result, value, key) => {
const newKey = key as keyof TextComponentProps
const item = mapPropsToForms[newKey]
if (item) {
const { valueProp = 'value', eventName = 'change', initalTransform, afterTransform } = item
const newItem: FormProps = {
...item,
value: initalTransform ? initalTransform(value) : value,
valueProp,
eventName,
events: {
['on' + capitalizeFirstLetter(eventName)]: (e: any) => { context.emit('change', { key, value: afterTransform? afterTransform(e) : e })}
}
}
result[newKey] = newItem
}
return result
}, {} as { [key: string]: FormProps })
})
return () =>
<div class="props-table">
{
Object.keys(finalProps.value).map(key => {
const value = finalProps.value[key]
const ComponentName = mapToComponent[value.component]
const SubComponent = value.subComponent ? mapToComponent[value.subComponent] : null
const props = {
[value.valueProp]: value.value,
...value.extraProps,
...value.events
}
return (
<div key={key} class="prop-item">
{ value.text && <span class="label">{value.text}</span> }
<div class="prop-component">
<ComponentName {...props}>
{ value.options &&
value.options.map(option => {
return (
<SubComponent value={option.value}>{option.text}</SubComponent>
)
})
}
</ComponentName>
</div>
</div>
)
})
}
</div>
}
})
方案二:使用 render 函数实现桥梁
增加返回render的Vnode
import { defineComponent } from 'vue'
const renderNode = defineComponent({
name: 'render-node',
props: {
vNode: {
type: [Object, String],
required: true,
},
},
render() {
return this.vNode
},
})
export default renderNode
修改{{option.text}}成render-node组件
<template>
<div class="props-table">
<div v-for="(item, index) in finalPros" class="prop-item" :key="index">
<span class="label" v-if="item.text">{{ item.text }}</span>
<div class="prop-component">
<!--v-bind 绑定属性 v-on 绑定事件-->
<component
v-if="item.valueProp"
:is="item?.component"
:value="item?.valueProp"
v-bind="item.extraProps"
v-on="item.events"
>
<template v-if="item.options">
<component
v-for="(option, k) in item.options"
:is="item.subComponent"
:value="option.value"
:key="k"
>
<render-node :vNode="option.text"></render-node>
</component>
</template>
</component>
</div>
</div>
</div>
</template>
<script lang="ts">
...
export default defineComponent({
name: 'props-table',
...
components: { RenderNode },
...
})
</script>
处理编辑时跳转问题
import { computed } from 'vue'
import _ from 'lodash-es'
export default function useComponentCommon<T extends { [key: string]: any }>(
props: T,
picks: string[],
) {
const styleProps = computed(() => _.pick(props, picks))
const handleClick = () => {
if (
window.location.hash !== '#/editor' &&
props.actionType === 'url' &&
props.url
) {
window.location.href = props.url
}
}
//事件类型:无|跳转URL下拉菜单
return { styleProps, handleClick }
}
修改data数据,字体为空时不显示数据
...
export const testComponents: ComponentData[] = [
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello',
fontSize: '20px',
color: '#000000',
lineHeight: '1',
textAlign: 'left',
fontFamily: '宋体',
},
},
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello2',
fontSize: '10px',
fontWeight: 'bold',
lineHeight: '2',
textAlign: 'left',
fontFamily: '宋体',
},
},
{
id: uuidv4(),
name: 'l-text',
props: {
text: 'hello3',
fontSize: '15px',
actionType: 'url',
url: 'https://www.baidu.com',
lineHeight: '3',
textAlign: 'left',
fontFamily: '宋体',
},
},
]
...
export default editor
总结:
业务组件
- 创建编辑器 vuex store 结构,画布循环展示组件
- 组件初步实现,使用 lodash 分离样式属性
- 添加通用和特殊属性,转换为 props 类型
- 抽取重用逻辑,style 抽取和点击跳转
- 左侧组件库点击添加到画布的逻辑
组件属性对应表单组件的展示和更新
- 获得正在被编辑的元素,通过 vuex getters
- 创建属性和表单组件的对应关系
- 使用 propsTable 将传入的属性渲染为对应的表单组件
- 丰富对应关系字段支持更多自定义配置
- 使用标准流程更新表单并实时同步单项数据流
- 使用 h 函数以及 vnode 实现字体下拉框实时显示
存在问题:设置为0或者’’时,组件会消失,后续记得改