med-mt
v0.8.4
Published
模板编辑工具
Downloads
36
Readme
模板编辑工具
背景目的
互联网医院商城移动端需要运营人员动态配置展示内容,从展示哪些内容,内容的展示顺序,内容的数据几个维度上都需要是灵活可变的。当然运营不可能 coding 页面,所以需要一套动态的可视化编辑器来帮助他们完成这些工作。
现阶段目标:
- [x] 图形化编辑,拖拽排序等功能
- [x] 可扩展控件
- [x] 可扩展数据类型
- [x] 效果预览
- [x] JSON 格式数据,支持 SSR
- [ ] 撤销,前进功能
- [ ] ...
概念
控件-Control
控件是客户端可见的界面,一个控件包含一套完成的页面和内部的功能逻辑。比如轮播
,推荐商品
等都是控件。控件表面和组件类似,但控件是组件和一系列配置的集合,因为控件除了展示页面,还需要告诉编辑器 我是谁(组件的身份)
、我有哪些数据类型和可编辑
,页面和编辑的数据是如何关联的
等问题。
现阶段控件会铺满容器。
结构
一个基础的控件文件夹包含以下文件
├── ControlName.tsx // 组件实现
├── config.json // 配置文件
├── index.ts // 入口文件
└── 其他依赖文件
配置文件
下面是配置的属性
| 属性 | 类型 | 说明 | 必填 | 默认值 | | -------- | ------ | ------------------------------------------------ | ---- | ------ | | name | string | 控件的唯一名称,相当于 id | 是 | - | | showName | string | 控件的显示名称,不传则取 name | 否 | - | | group | string | 控件所在的分类,相同分类的控件会归纳在同一个分组 | 否 | 默认 | | thumb | string | 控件展示的缩略图 | 是 | - | | props | array | 定义控件的依赖的属性,是控件配置的核心 | 是 | - |
配置 props
props
声明了控件 UI 依赖的属性值,声明的类型的转化成值会以 props 的形式传递给 React UI 组件,也就是说,这里定义了组件的每一个 prop 的类型。
每一个 prop 至少包含key
和schema
两个属性。
key
属性的键,比如 key 是url
,那么 UI 组件会接受到一个url
的 propschema
属性的类型,比如SchemaTypes.Text
声明了这个值是一个字符串。schema 不仅决定了值的类型,还决定了在编辑这个值时页面上是如何展示的,如文本框,选择器等等。此外我们还可以自定义 schema,这个后面会提到。
基本的配置
{
props: [
{
key: 'url',
schema: SchemaTypes.Text,
},
]
}
当类型有扩展属性时,可以将 schema 以 type
字段声明,其他字段作为扩展属性,如定义inputProps
传递给输入框控件,内部有maxLength
为最大输入长度。
{
props: [
{
key: 'url',
schema: {
type: SchemaTypes.Text,
inputProps: {
maxLength: 4, // 最大长度为4
},
},
},
]
}
schema
还支持嵌套声明,不过由于多层嵌套在 UI 层面不好展示层级关系,因此目前只支持一层嵌套。如分类 1有name
和link
2 个属性。
{
props: [
{
key: 'imageLink',
schema: {
name: {
type: SchemaTypes.Text,
inputProps: {
maxLength: 4, // 最大长度为4
},
},
link: {
type: SchemaTypes.Link,
},
},
},
]
}
我们默认支持了数组类型的schema
,当类型为数组时,会有个额外的item
属性,item
的配置和schema
配置完全一致。此外还可以通过max
属性来限制最大长度。
如一个可编辑长度的分类控件配置如下
{
props: [
{
key: 'categories',
schema: {
type: SchemaTypes.Array,
max: 8,
item: {
name: {
type: SchemaTypes.Text,
inputProps: {
maxLength: 4, // 最大长度为4
},
},
link: {
type: SchemaTypes.Link,
},
},
},
},
]
}
当数组类型在界面上初始化时,编辑器会生成一个默认项,当长度为 1 时最后一项将无法删除,也就是说数组值的长度永远不会为 0,除非删除整个控件。
除了 type 外还有一些通用的属性
| 属性 | 类型 | 说明 | 必填 | 默认值 | | -------- | ------ | --------------- | ---- | ------ | | default | any | schema 的默认值 | 否 | - | | showName | string | schema 的标签 | 否 | - | | tips | string | 控件的提示文案 | 否 | - |
控件组件
控件组件实际上就是一个 React 组件,上面已经提到组件的props
就是 config 中配置的props
属性。
例如上面的链接图片控件的组件实现大概是这样的
import React from 'react'
import './style.css'
interface Props {
url: string
link: string
}
export default (props: Props) => {
const { url, link } = props
return (
<Link href={link} className="mt-full-image-box">
<img src={url} className="mt-full-image" alt="" />
</Link>
)
}
当编辑者选中控件到**视窗面板(vision panel)**后,页面会渲染这个组件的内容。如果点击控件,**编辑视图(editor panel)**会出现控件定义的 props 的编辑控件用于编辑。
Interactive
默认支持控件的整体选中进行编辑,但是有时候我们的控件上可定义的元素会比较多,这时让使用者判断当前编辑的哪一部分内容会比较困难。为了解决这个问题,我们提供了一个Interactive
组件来解决这个问题。它有以下功能
- 被包裹的组件会成为一个可交互的元素,当点击时会定位到元素对应的编辑控件
- 当元素对应的编辑控件获取焦点时,元素也会高亮
- 元素属性更改时,高亮部分也会随属性变化而变化
Interactive
组件并不会对组件本身的渲染造成影响,在实际运行时相当于直接返回了被包裹的组件本身。
它的使用也非常的简单,下面以分类控件来演示用法。
categories.map((item, index) => {
return (
<Interactive path={`categories[${index}].link`} key={index}>
<div className="mt-category-auto-item">
<Interactive path={`categories[${index}].icon`}>
<img className="mt-category-auto-item-icon" src={item.icon} alt="" />
</Interactive>
<Interactive path={`categories[${index}].name`}>
<div className="mt-category-auto-item-text">{item.name}</div>
</Interactive>
</div>
</Interactive>
)
})
将需要显示的组件用 Interactive 进行包裹,并传入path
属性,path
是当前组件依赖的属性路径。如第三个分类的图标的path
为categories[2].icon
。因为链接是针对于整个数组有效,所以在最外层包裹并设置path
为categories[2].link
。
导出和注册
从index.ts
导出控件的组件和配置
import Component from './Component'
import config from './config'
export default {
Component,
config,
}
当在页面入口通过regsitControls
注册组件后,在**控件面板(control panel
)**便能看到所有控件。
// App.tsx
mt.regsitControls([fullImage])
数据定义-Schema
Schema 定义控件某个属性的类型,它确定了编辑时的 UI 和产生的数据。
Schema 组件
Schema 通过一个 React 组件实现功能,下面是文本输入框的 schema 实现
import React, { useCallback, useRef } from 'react'
import './style.css'
import { useSchemaFocus, SchemaTypes } from '../../core/schema'
export default (props: SchemaCompPropsWithConfig) => {
const { schemaDefinition, value, onChange } = props
const { inputProps, tips, showName } = schemaDefinition
const inputRef = useRef<HTMLInputElement>(null)
const handleFocus = useCallback(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
const IS_NUMBER = schemaDefinition.type === SchemaTypes.Number
const { focus, blur } = useSchemaFocus(handleFocus)
return (
<div className="mt-form-el">
<label>
{showName && <span className="mt-form-el-label">{showName}: </span>}
<input
ref={inputRef}
value={value === undefined || value === null ? '' : value}
className="mt-input-text"
type="text"
placeholder={`请输入${schemaDefinition.showName || ''}`}
{...inputProps}
onChange={e => {
const value = Number(e.target.value)
if (IS_NUMBER) {
console.log(value)
onChange(isNaN(value) ? 0 : value)
} else {
onChange(e.target.value)
}
}}
onBlur={blur}
onFocus={focus}
/>
{tips && <div className="mt-form-el-tips">* {tips}</div>}
</label>
</div>
)
}
Schema 的实现内容没有强制要求,每一个 Schema 组件会被传入一下属性
| 属性 | 类型 | 说明 | 必填 | 默认值 | | ---------------- | ----------------- | --------------- | ---- | ------ | | schemaDefinition | any | schema 的默认值 | 否 | - | | value | any | 值 | 否 | - | | onChange | (value:any)=>void | 值更改的回调 | 否 | - |
Schema 组件会有 2 种控制的状态
- 获取焦点 - 当编辑视窗的控件中的某个可交互的(Interactive)控件元素进行点击时,schema 组件会获取焦点
- 失去焦点 - 当其他 schema 控件获取焦点或者对应控件元素失去焦点时,当前 schema 组件会失去焦点。
我们通过useSchemaFocus
hook 来控制 Schema 的获焦和失焦行为。它接受一个回调函数,当控件元素获取焦点时会触发。同时返回focus
和blur
方法,如果 schema 组件在操作时获取和失去焦点时(如输入框获取了焦点),执行对应的方法能时让编辑视窗的控件获取焦点。
建议使用浏览器原生获取焦点的能力来避免怪异行为,例如
tabindex
等。。
Schema 注册
schema 需要注册后才能在控件中使用,也就是说必须在控件注册之前注册 schema。
注册后的 schema 可以在SchemaTypes
获取。
registSchemaComp('DrugList', DrugList)
registSchemaComp('DrugCategory', DrugCategory)
// 使用
{
type: SchemaTypes.DrugList,
}
其他组件
Link
Link 组件用于解决编辑过程时 a 标签点击问题
useEnv
获取当前的环境上下文,如是编辑模式(edit)还是渲染模式(render)。