先看看效果
整体布局
目录 index 代码主体 由于涉及到夸组件联动 所以使用了createContext存储
const TemplateContext = createContext(null)
const TemplateSettings: React.FC<{}> = () => {
const actionRef = useRef(null)
const [currentControl, setCurrentControl] = useState<controlDataType>();
const setControlVal = (val: controlDataType) => {
if (actionRef?.current?.setControlIndexVal) {
actionRef.current.setControlIndexVal(val)
}
}
// currentControl :当前选中的组件 setCurrentControl:设置选中组件的值
// setControlVal :设置模板当前选中dom的虚拟数据
return <PageHeaderWrapper>
<TemplateContext.Provider value={{ currentControl, setCurrentControl, setControlVal }}>
<div className={styles.settingsMain}>
<div className={styles.left}>
<Sidebar ></Sidebar>
</div>
<div className={styles.center}>
<CanvasContent actionRef={actionRef}></CanvasContent>
</div>
<div className={styles.right}>
<AttrConfig ></AttrConfig>
</div>
</div>
</TemplateContext.Provider>
</PageHeaderWrapper>
}
左侧菜单
左侧导航栏主要是提供组件 实现拖拽 并存储组件参数
const TemplateSidebar: React.FC<{}> = () => {
const [controlList, setControlList] = useState<controlType[]>([]) //控件列表
const onDragStart = (evt: React.DragEvent, item: any) => { // 拖拽事件
evt.dataTransfer.setData('application/template', JSON.stringify(item));
evt.dataTransfer.effectAllowed = 'move';
}
const getControlList = useCallback(() => {
return controlList.map((item: controlType, index) => <Col span={12} key={index}>
<div className={styles.control} onDragStart={(event) => onDragStart(event, item)} draggable>
<span className={`icon iconfont ${item.iconName}`}></span>{item.contentName}
</div>
</Col>)
},[controlList])
useEffect(() => {
setControlList([{
contentName: '文本',
iconName: 'icon-ziti',
contentType: 1,
contentTypeName: 'text'
}, {
contentName: '文本域',
iconName: 'icon-ziti',
contentType: 2,
contentTypeName: 'textarea'
},
{
contentName: '选择(单选)',
iconName: 'icon-ziti',
contentType: 5,
contentTypeName: 'select',
templateOptionList: []
},
{
contentName: '日期组件',
iconName: 'icon-ziti',
contentType: 9,
contentTypeName: 'contact_multiple',
},
])
}, []);
return <div className={styles.templateSidebar}>
<div className={styles.title}>
组件库
</div>
<div className={styles.typeName}>
基础组件
</div>
<Row className={styles.controlMain}>
{
getControlList && getControlList()
}
</Row>
</div>
}
export default TemplateSidebar
中间画布
中间内容区域 分了两个部分 一个是画布用于接收拖拽的组件 一个负责展示拖拽的组件并实现可视化排序
画布组件
const CanvasContent: React.FC<{ actionRef: any }> = (props) => {
const { actionRef } = props
const [controlDataList, setControlDataList] = useState<controlDataType[]>([]) // 组件数据
const childRef = useRef(null)
if (actionRef) {
useImperativeHandle(actionRef, () => ({ // 联动设置当前选中的组件参数
setControlIndexVal: (val: controlDataType) => {
const currentIndex = childRef.current.getCurrentIndex()
setControlVal(val, currentIndex)
},
}))
}
const onDrop = (event: React.DragEvent) => { // 画布接收拖拽放开后的参数 分为左侧导航栏拖拽的 和内容区域拖拽排序的
event.preventDefault();
if (event.dataTransfer.getData('application/template')) {//导航栏拖拽
const itemTemplate = JSON.parse(event.dataTransfer.getData('application/template'));
const { hoverIndex } = childRef.current.getHoverIndex() //获取鼠标当前滑动到的位置
let arr = controlDataList
let objCount = {}
arr.forEach((item) => {
if (objCount[item.contentTypeName]) {
objCount[item.contentTypeName]++;
} else {
objCount[item.contentTypeName] = 1
}
})
const count = (objCount[itemTemplate.contentTypeName] || 0) + 1 //生成KEY值
let template = { ...itemTemplate, filedMapping: itemTemplate.contentTypeName + '_' + count, isEnable: 1, isRequired: 1, message: '', prompt: '' }
if (hoverIndex > -1) { //插入数据
arr.splice(hoverIndex, 0, template);
} else {
arr.push(template)
}
setControlDataList([...arr])
childRef.current.hoverEnd() //重置滑动下标
} else if (event.dataTransfer.getData('application/moveControl')) {
let arr = controlDataList
const { hoverIndex, currentIndex } = childRef.current.getHoverIndex()// 获取鼠标当前滑动到的位置 和拖拽起始位置
if (currentIndex === (hoverIndex - 1)) {
return
}
if (hoverIndex === -1 || currentIndex === -1) {
childRef.current.hoverEnd()
return
} else if (hoverIndex === arr.length) { //此时是移动到最后位置的处理
const temp = arr[currentIndex];
arr.splice(currentIndex, 1);
arr.push(temp)
} else {
const temp = arr[currentIndex];//基于滑动位置插入 删除原有位置数据
arr[currentIndex] = undefined;
arr.splice(hoverIndex, 0, temp);
arr = arr.filter((item) => item)
setControlDataList([...arr])
}
childRef.current.hoverEnd()
}
}
const onDragOver = useCallback((event) => { // 拖动中事件
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const setControlVal = (val: controlDataType, index: number) => { //根据下标设置参数
let arr = deepCopy(controlDataList)
arr[index] = { ...val }
setControlDataList([...arr])
}
const onCopyAdd = (itemData: controlDataType, index: number) => { // 复制组件
let arr = deepCopy(controlDataList)
let objCount = {}
arr.forEach((item: controlDataType) => {
if (objCount[item.contentTypeName]) {
objCount[item.contentTypeName]++;
} else {
objCount[item.contentTypeName] = 1
}
})
const count = (objCount[itemData.contentTypeName] || 0) + 1
arr.splice(index, 0, { ...deepCopy(itemData), filedMapping: itemData.contentTypeName + '_' + count });
setControlDataList([...arr])
}
const onDellData = (itemData: controlDataType, index: number) => { // 删除组件
let arr = controlDataList
arr.splice(index, 1);
childRef.current.hoverEnd()
setControlDataList([...arr])
}
return <div className={styles.canvasContent} onDragOver={(event) => onDragOver(event)} onDrop={(event) => onDrop(event)} >
<FormTemplate actionRef={childRef} onDellData={onDellData} onCopyAdd={onCopyAdd} dataArr={controlDataList} setControlVal={setControlVal} ></FormTemplate>
</div>
}
模板渲染组件 该组件稍微复杂点的功能就是拖拽插入排序了 核心就是记录插入的下标 目前设计的是向上插入 所以底部需要加个容器是的可以拖拽至底部
const FormTemplate: React.FC<FormTemplateProps> = (props) => {
const [templateForm] = Form.useForm()
const [checkFiledMapping, setCheckFiledMapping] = useState<string>('');
const [activateIndex, setActivateIndex] = useState<number>(-1);
const [hoverIndex, setHoverIndex] = useState<number>(-1);
const { currentControl, setCurrentControl, } = useContext(TemplateContext)
const { dataArr, actionRef, onCopyAdd, onDellData } = props
const startRef = useRef(null);
if (actionRef) {
useImperativeHandle(actionRef, () => ({
getHoverIndex: () => { // hoverIndex:获取鼠标拖拽时当前下标 currentIndex:组件拖拽起始下标
return { hoverIndex, currentIndex: startRef.current };
},
getCurrentIndex: () => { // 获取当前选中下标
return activateIndex;
},
hoverEnd: () => { // 重置拖拽时下标
setHoverIndex(-1)
}
}))
}
const onClickItem = (event: React.BaseSyntheticEvent, controlItem: controlDataType, index: number) => {
// 选中组件事件
event.stopPropagation(); //防止冒泡触发父组件点击事件
setCheckFiledMapping(controlItem.filedMapping)
setCurrentControl({ ...deepCopy(controlItem) })
setActivateIndex(index)
}
const onDragStart = (evt: React.DragEvent, item: controlDataType, index: number) => {
// 组件拖拽初始事件 记录初始下标
startRef.current = index;
evt.dataTransfer.setData('application/moveControl', JSON.stringify(item));
evt.dataTransfer.effectAllowed = 'move';
}
const onDragEnd = (evt: React.DragEvent, index: number) => {
evt.preventDefault();
}
const onDragEnter = (evt: React.DragEvent, index: number) => {
// 组件拖拽时事件 记录插入下标
evt.preventDefault();
if (startRef.current === index) {
setHoverIndex(-1)
return;
}
setHoverIndex(index)
}
const onCopy = (event: React.BaseSyntheticEvent, item: controlDataType, index: number) => {
event.stopPropagation();
if (onCopyAdd) {
onCopyAdd(item, index)
}
}
const onDell = (event: React.BaseSyntheticEvent, item: controlDataType, index: number) => {
event.stopPropagation();
if (onDellData) {
onDellData(item, index)
}
}
const getFormItem = () => {
return dataArr.map((item: controlDataType, index: number) => {
//activate:当前选中的元素 noEnable:是否禁用 thingHover:拖拽时的插入提示
return (
<div className={classNames(styles.thing, checkFiledMapping === item.filedMapping ? styles.activate : ''
, item.isEnable === 1 ? '' : styles.noEnable, hoverIndex === index ? styles.thingHover : '')}
onDragEnter={(event) => onDragEnter(event, index)}
onDragEnd={(event) => onDragEnd(event, index)}
onDragStart={(event) => onDragStart(event, item, index)} draggable
onClick={(event) => onClickItem(event, item, index)} key={item.filedMapping}>
<div className={styles.handleCell}>
<span className={styles.copy} onClick={(event) => onCopy(event, item, index)}>复制</span>
<span className={styles.dell} onClick={(event) => onDell(event, item, index)}>删除</span>
</div>
<Form.Item
label={item.contentName + ':'}
name={item.filedMapping}
rules={[{ required: item.isRequired === 1, message: item.message }]}
>
{getFormControl(item)}
</Form.Item>
</div>
)
})
}
const getFormControl = useCallback((item: controlDataType) => {
switch (item.contentType) {
case 1:
return (
<Input
readOnly
placeholder={item.prompt}
autoComplete="off"
/>
)
case 2:
return (
<Input.TextArea
readOnly
autoSize={{ minRows: 2, maxRows: 18 }}
showCount={true}
placeholder={item.prompt}
/>
)
case 5:
return (
<Select
open={false}
options={[]}
placeholder={item.prompt}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
/>
)
case 9:
return <DatePicker disabled placeholder={item.prompt}
default:
return null
}
}, [])
const formTemplateClick = () => {
if (checkFiledMapping) {
setCheckFiledMapping('')
setCurrentControl({})
setActivateIndex(-1)
}
}
return <div className={styles.formTemplate} onClick={() => formTemplateClick()}>
<Form layout="vertical" colon={false} form={templateForm}>
<ConfigProvider locale={locale}>
{getFormItem && getFormItem()}
{
//增加一个底部容器 因为都是向上插入 导致无法增加到底部 所以加个空白容器 使其能够拖拽至底部
dataArr.length > 0 && <div className={classNames(styles.bottomItem, hoverIndex === dataArr.length ? styles.thingHover : '')} onDragEnter={(event) => onDragEnter(event, dataArr.length)}>
</div>
}
</ConfigProvider>
</Form>
</div>
}
右侧配置栏
这个就没什么难点了 基本就是form表单跟画布组件数据联动 后续需要拓展配置参数可在该组件实现
const AttrConfig: React.FC<AttrConfigProps> = (props) => {
const [configForm] = Form.useForm()
const { currentControl, setControlVal } = useContext(TemplateContext)
const [isShow, setIsShow] = useState<boolean>(false);//是否展示
const onSaveConfig = () => {
configForm.validateFields().then((values) => {
const data = deepCopy({ ...currentControl, ...values })
setControlVal({ ...data }) //保存进行数据联动
})
}
const getExtraItem = () => {
const { contentType } = currentControl
if (contentType === 5) { //根据类型控制展示设置项
return <Form.Item
label="下拉选项"
name="templateOptionList"
initialValue={[]}
key={currentControl.filedMapping}
rules={[
{
required: true,
message: '请输入要新增的选项',
validator(rule, value) {
if (value) {
return Promise.resolve();
}
return Promise.reject();
}
}
]}
>
<Select mode="tags" labelInValue options={[]} />
</Form.Item>
} else {
return null
}
}
const getFormItem = useCallback(() => {
if (isShow) {
return <>
<Form.Item name="contentName" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
<Input />
</Form.Item>
<Form.Item label="标签类型" >
<span>
{typeMap[currentControl.contentType]}
</span>
</Form.Item>
{getExtraItem && getExtraItem()}
<Form.Item
label="是否必填"
name="isRequired"
valuePropName="checked"
normalize={(value: any) => Number(value)}
>
<Switch
checkedChildren={isRequiredMap[1]}
unCheckedChildren={isRequiredMap[0]}
/>
</Form.Item>
{currentControl.contentType !== 15 && <Form.Item name="prompt" label="占位提示" >
<Input />
</Form.Item>}
<Form.Item
label="是否启用"
name="isEnable"
valuePropName="checked"
normalize={(value: any) => Number(value)}
>
<Switch
checkedChildren={isEnableMap[1]}
unCheckedChildren={isEnableMap[0]}
/>
</Form.Item>
<Row justify="center">
<Button type="primary" onClick={() => onSaveConfig()}>保存</Button>
</Row>
</>
} else {
return null
}
}, [isShow, currentControl])
useEffect(() => {
if (currentControl && Object.keys(currentControl).length) {
//切换组件时更新设置项参数
setIsShow(true)
configForm.setFieldsValue({ ...currentControl })
} else {
setIsShow(false)
}
}, [currentControl]);
return <div className={styles.attrConfig}>
<Form form={configForm} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} layout="horizontal" name="nest-messages" >
{getFormItem && getFormItem()}
</Form>
</div >
}