属性更新
- 属性编辑通过
store
获取属性值 - 通过发射事件触发
commit
修改属性值 - 支持属性值的转换
propsMap.ts
代码语言:javascript复制import type { TextComponentProps } from './defaultProps';
// 属性转化成表单 哪个属性使用哪个类型的组件去编辑
export interface PropsToForm {
component: string;
subComponent?: string;
extraProps?: { [key: string]: any };
text?: string;
options?: {
text: string;
value: any;
}[];
initalTransform?: (v: 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: (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: [
{
value: '',
text: '无',
},
{
value: '"SimSun","STSong',
text: '宋体',
},
{
value: '"SimHei","STHeiti',
text: '黑体',
},
],
afterTransform: (e: any) => e,
},
};
PropsTable.vue
代码语言:javascript复制<template>
<div class="props-table">
<div v-for="(item, index) in finalProps"
class="prop-item"
:key="index">
<span class="label">{{ item.text }}</span>
<div class="prop-component">
<!-- 使用 antd 组件库中的组件 -->
<component v-if="item.valueProp"
:[item.valueProp]="item?.value"
v-bind="item?.extraProps"
v-on="item.events"
:is="item?.component">
<template v-if="item.options">
<component :is="item.subComponent"
v-for="(option, key) in item.options"
:key="key"
:value="option.value">
{{ option.text }}
</component>
</template>
</component>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, computed, defineEmits } from 'vue';
import { mapPropsToForms } from '../propsMap'
import { reduce } from 'lodash-es'
import type { PartialTextComponentProps } from '../defaultProps'
export interface Props {
props: PartialTextComponentProps;
}
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 };
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'change', data: any): void;
}>()
// 获取属性表单映射列表
const finalProps = computed(() => {
return reduce(props.props, (result, value, key) => {
const newKey = key as keyof PartialTextComponentProps
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: {
[eventName]: (e: any) => {
emit('change', { key, value: afterTransform ? afterTransform(e) : e })
}
}
}
result[newKey] = newItem
}
return result
}, {} as { [key: string]: FormProps })
})
</script>
Editor.vue
代码语言:javascript复制// template
<!-- 右侧组件属性编辑 -->
<a-layout-sider width="300"
style="background:#fff"
class="setting-container">
组件属性
<props-table v-if="currentElement"
@change="handleChange"
:props="currentElement?.props"></props-table>
</a-layout-sider>
// script
// 更新组件的属性值事件
const handleChange = (e: any) => {
store.commit('updateComponent', e)
}
editor.ts
代码语言:javascript复制mutations: {
// 更新组件属性
updateComponent(state, { key, value }) {
const updatedComponent = state.components.find(
(c) => c.id === state.currentElement,
);
if (updatedComponent) {
updatedComponent.props[key as keyof TextComponentProps] = value;
}
},
},
最终实现如下
优化需求
选择字体的下拉框可以直接显示当前的字体样式
h 函数
h
函数接收三个参数
- type: 元素的类型
- props: 数据对象
- children: 子节点
使用 h
函数改写
import { h, VNode } from 'vue';
const fontFamilyArr = [
{
value: '"SimSun","STSong',
text: '宋体',
},
{
value: '"SimHei","STHeiti',
text: '黑体',
},
];
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
使用 tsx
改写
const fontFamilyOptions = fontFamilyArr.map((font) => {
return {
value: font.value,
text: (<span style={{ fontFamily: font.text }}>{font.text}</span>) as VNode,
};
});
使用 render 函数实现桥梁
代码语言:javascript复制// srccomponentsRenderVnode.ts
import { defineComponent } from 'vue';
const RenderVnode = defineComponent({
props: {
vNode: {
type: [Object, String],
required: true,
},
},
render() {
return this.vNode;
},
});
export default RenderVnode;
代码语言:javascript复制// srccomponentsPropsTable.vue
<!-- 使用 antd 组件库中的组件 -->
<component v-if="item.valueProp"
:[item.valueProp]="item?.value"
v-bind="item?.extraProps"
v-on="item.events"
:is="item?.component">
<template v-if="item.options">
<component :is="item.subComponent"
v-for="(option, key) in item.options"
:key="key"
:value="option.value">
<render-vnode :vNode="option.text"></render-vnode>
</component>
</template>
</component>
最终实现如下
阶段总结
业务组件
- 创建编辑器
vuex store
结构,画布循环展示组件 - 组件初步实现,使用
lodash
分离样式属性 - 添加通用和特殊属性,转换为
props
类型 - 抽取重用逻辑,
style
抽取和点击跳转 - 左侧组件库点击添加到画布的逻辑
组件属性对应表单组件的展示和更新
- 获得正在被编辑的元素,通过
vuex getters
- 创建属性和表单组件的对应关系
- 使用
propsTable
将传入的属性渲染为对应的表单组件 - 丰富对应关系字段支持更多自定义配置
- 使用标准流程更新表单并实时同步单项数据流
- 使用
h
函数以及vnode
实现字体下拉框实时显示