1.当选中的画布组件变化时 onCanvasSelect
1.1 需求:因为在实际操作过程中,拖拽后的组件id是必须存在且不能重复,这个随机id值不能作为和后端交互的标识,需要自定义一个组件,用于存放后端的标识。这个id只能在前端当作唯一key。
通过这个api,可以获取当前选中组件的schema值,通过设置状态保存当前选中的唯一标识id,为了后续拿值作准备
const onCanvasSelect = (schema) => {
// 获取当前选中的组件唯一ID
// console.log(schema.$id?.split('/')[1], 'onCanvasSelect', schema);
// 保存当前选中的标识
setCurrentSelect(schema.$id?.split('/')[1]);
};
2.当表单schema变化时 onSchemaChange
2.1 需求:侧边通过自定义初始配置,添加了下拉选择表单属性,每当拖拽一个组件,都需要选择一个属性值,并且每个属性值只能使用一次。例如,下拉选项3个属性值,已经使用一个,那么当选中当前组件时,只会展示剩余的两个。
2.2 逻辑实现:
1.根据保存的标识,通过schema值,获取到当前操作组件的属性。
2.获取已使用的属性值,判断当选择的数据库发生改变时,可以调接口,自定义配置schema,例如:添加额外属性配置,正则表达式限制。
3.需要判断当前的表属性是否存在,并且改变时才要重新set,不然会造成不管修改了什么属性页面都会重新set的bug
4.封装公共的方法,根据全部属性和使用属性,找出剩余的属性并重新设置
5.深度遍历判断两个对象是否相等
2.3 代码实现
const onSchemaChange = (value) => {
// console.log(value, 'onSchemaChange');
let transSchema = JSON.parse(JSON.stringify(mySchema));
// 判断当前选择的值是否为空
if (JSON.stringify(value) != '{}') {
let transSelect = value.properties[currentSelect];
// console.log(transSchema, 'transSchema', value, 'transSelect');
// 获取已使用属性
let useProperty = [];
for (let key in value.properties) {
let target = value.properties[key];
if (target.weightProperty) {
useProperty.push(target.weightProperty);
}
}
// console.log(useProperty,'useProperty')
// 判断是否选择对应的库
if (selectValue) {
// 判断当前点击的组件是否为上一个组件,判断当前选择的库是否为新组件,就组件中是否存在属性,并且判断当前组件选择的库是否发生改变
// console.log(transSchema, transSelect);
if (
JSON.stringify(transSchema) == '{}' ||
!transSchema.properties[currentSelect] ||
(transSelect &&
!deepEqual(transSchema.properties[currentSelect], transSelect))
) {
// 判断当选择的数据库发生改变时,可以调接口,自定义配置schema,例如:添加额外属性配置,正则表达式限制
console.log('存在即证明');
// 需要判断当前的表属性是否存在,并且改变时才要重新set,不然会造成不管修改了什么属性页面都会重新set的bug
if (
transSelect?.weightProperty &&
(JSON.stringify(transSchema) == '{}' ||
transSelect?.weightProperty !=
transSchema.properties[currentSelect]?.weightProperty)
) {
properList.length != 0 &&
properList.forEach((m) => {
if (transSelect.weightProperty == m.id) {
// 设置回显时需要的数据
value.properties[currentSelect].title = m.attrDescribe;
value.properties[currentSelect].formId = m.attrName;
// 根据返回的类型,添加限制条件
if (m.attrType == 'varchar') {
value.properties[currentSelect].max = 65535;
value.properties[currentSelect].min = 1;
value.properties[currentSelect].required = true;
} else if (m.attrType == 'float') {
value.properties[currentSelect].min = 1;
let rules = [
{ required: true, message: '不能为空!' },
{
pattern: '^[0-9]{0,9}[.][0-9]{1,9}$',
message: '只能输入float类型!',
},
];
value.properties[currentSelect].rules = rules;
value.properties[currentSelect].required = true;
} else if (m.attrType == 'int') {
value.properties[currentSelect].min = 1;
value.properties[currentSelect].max = 5;
let rules = [
{ required: true, message: '不能为空!' },
{
pattern: '^[0-9]{0,5}$',
message: '只能输入int类型!',
},
];
value.properties[currentSelect].rules = rules;
value.properties[currentSelect].required = true;
}
setMySchema(value);
}
});
}
actionSettting(useProperty);
}
}
}
};
// 封装公共的方法,根据全部属性和使用属性,找出剩余的属性并重新设置
const actionSettting = (useProperty) => {
let transProperList = JSON.parse(JSON.stringify(properList)).filter(
(m) => !useProperty.includes(m.id),
);
let transEnum = transProperList.map((m) => m.id);
let transEnumName = transProperList.map((m) => m.attrDescribe);
setFormSetting({
weightProperty: {
title: '关联数据表',
type: 'string',
enum: transEnum,
enumNames: transEnumName,
weight: 'select',
},
...commonSettings,
});
};
// 深度遍历判断两个对象是否相等
function deepEqual(object1, object2) {
let keys1 = Object.keys(object1);
let keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let index = 0; index < keys1.length; index++) {
let val1 = object1[keys1[index]];
let val2 = object2[keys2[index]];
let areObjects = isObject(val1) && isObject(val2);
if (
(areObjects && !deepEqual(val1, val2)) ||
(!areObjects && val1 !== val2)
) {
return false;
}
}
return true;
}
function isObject(object) {
return object != null && typeof object === 'object';
}
3.删除事件 canDelete
3.1 遇到的问题:当删除组件时,表单schema会监听到变化,捕获到的当前标识为undefined,或者自动跳转到当前拖拽的第一个组件标识,所以造成页面无法重新actionSettting
3.2 解决方法: 在删除的api里监听到事件
3.3 代码
// 删除操作,因为在删除时,表单的onSchemaChange方法setFormSetting时页面不会重新渲染,只能在删除内操作
const canDelete = () => {
let transSchema = JSON.parse(JSON.stringify(mySchema));
if (transSchema) {
let useProperty = [];
for (let key in transSchema.properties) {
let target = transSchema.properties[key];
// 因为这里是删除操作,当选中的组件id和当前删除的id不一致时,才可以添加
if (
target.weightProperty &&
target.weightProperty !=
transSchema.properties[currentSelect]?.weightProperty
) {
useProperty.push(target.weightProperty);
}
}
actionSettting(useProperty);
}
return true;
};
整个代码,可以copy测试
import { useEffect, useState, useCallback, useRef } from 'react';
import styles from './index.less';
import { history, useSelector, useDispatch } from 'umi';
import { Button, Modal, Table, Select, Spin, message } from 'antd';
import { ExportOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import commonSettings from './baseConfig/commonSettings';
import settings from './baseConfig/settings';
import globalSettings from './baseConfig/globalSettings';
import Generator from 'fr-generator';
const UserSystem = ({ location }) => {
const { id } = location.query;
const dispatch = useDispatch();
const genRef = useRef();
// 控制遮罩层
const [loading, setLoading] = useState(true);
// 下拉选择
const [selectValue, setSelectValue] = useState('');
// 退出弹窗控制
const [outTip, setOutTip] = useState(false);
// 保存、发布弹窗控制
const [actionTip, setActionTip] = useState(false);
const [status, setStatus] = useState('');
// 处理后的table数据
const [tableData, setTableData] = useState([]);
// 保存设置后的schema
const [mySchema, setMySchema] = useState({});
// 当前选中的组件
const [currentSelect, setCurrentSelect] = useState(undefined);
// 所有数据库列表
const unIssueList = useSelector((state) => state.userSystem.unIssueList);
// 表单配置
const [formSetting, setFormSetting] = useState({});
// 保存返回的数据表
const [properList, setProperList] = useState([]);
// 获取数据库列表
useEffect(() => {
dispatch({
type: 'userSystem/unIssueList',
payload: {
issue: 0,
},
}).then((res) => {
if (id && res && res.status === 20000) {
setSelectValue(String(id));
}
});
}, []);
// 监听选择的数据库,调数据表接口,重新设置配置项
useEffect(() => {
// console.log(selectValue, 'selectValue');
if (selectValue) {
let dataBase = JSON.parse(JSON.stringify(unIssueList));
let modelId = dataBase.filter((m) => m.id == selectValue);
setLoading(false);
dispatch({
type: 'userSystem/propertiesList',
payload: {
modelId: modelId[0].tableId,
},
}).then((res) => {
if (res && res.status === 20000) {
// 保存返回表数据源
setProperList(res.data);
// 将返回的数据,保存至schema格式
let transEnum = res.data.map((m) => m.id);
let transEnumName = res.data.map((m) => m.attrDescribe);
setFormSetting({
weightProperty: {
title: '关联数据表',
type: 'string',
enum: transEnum,
enumNames: transEnumName,
weight: 'select',
},
...commonSettings,
});
}
});
}
}, [selectValue]);
// 将保存schema,强制设置到页面
useEffect(() => {
if (JSON.stringify(mySchema) != '{}') {
genRef.current.setValue({
labelWidth: 120,
displayType: 'row',
...mySchema,
});
}
}, [mySchema]);
// 深度遍历判断两个对象是否相等
function deepEqual(object1, object2) {
let keys1 = Object.keys(object1);
let keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let index = 0; index < keys1.length; index++) {
let val1 = object1[keys1[index]];
let val2 = object2[keys2[index]];
let areObjects = isObject(val1) && isObject(val2);
if (
(areObjects && !deepEqual(val1, val2)) ||
(!areObjects && val1 !== val2)
) {
return false;
}
}
return true;
}
function isObject(object) {
return object != null && typeof object === 'object';
}
// 数据库列表下拉
const selectChange = (value) => {
setSelectValue(value);
if (!value) {
setLoading(true);
setFormSetting({
...commonSettings,
});
}
if (!value || value != selectValue) {
genRef.current.setValue({});
}
};
const onSchemaChange = (value) => {
// console.log(value, 'onSchemaChange');
let transSchema = JSON.parse(JSON.stringify(mySchema));
// 判断当前选择的值是否为空
if (JSON.stringify(value) != '{}') {
let transSelect = value.properties[currentSelect];
// console.log(transSchema, 'transSchema', value, 'transSelect');
// 获取已使用属性
let useProperty = [];
for (let key in value.properties) {
let target = value.properties[key];
if (target.weightProperty) {
useProperty.push(target.weightProperty);
}
}
// console.log(useProperty,'useProperty')
// 判断是否选择对应的库
if (selectValue) {
// 判断当前点击的组件是否为上一个组件,判断当前选择的库是否为新组件,就组件中是否存在属性,并且判断当前组件选择的库是否发生改变
// console.log(transSchema, transSelect);
if (
JSON.stringify(transSchema) == '{}' ||
!transSchema.properties[currentSelect] ||
(transSelect &&
!deepEqual(transSchema.properties[currentSelect], transSelect))
) {
// 判断当选择的数据库发生改变时,可以调接口,自定义配置schema,例如:添加额外属性配置,正则表达式限制
console.log('存在即证明');
// 需要判断当前的表属性是否存在,并且改变时才要重新set,不然会造成不管修改了什么属性页面都会重新set的bug
if (
transSelect?.weightProperty &&
(JSON.stringify(transSchema) == '{}' ||
transSelect?.weightProperty !=
transSchema.properties[currentSelect]?.weightProperty)
) {
properList.length != 0 &&
properList.forEach((m) => {
if (transSelect.weightProperty == m.id) {
// 设置回显时需要的数据
value.properties[currentSelect].title = m.attrDescribe;
value.properties[currentSelect].formId = m.attrName;
// 根据返回的类型,添加限制条件
if (m.attrType == 'varchar') {
value.properties[currentSelect].max = 65535;
value.properties[currentSelect].min = 1;
value.properties[currentSelect].required = true;
} else if (m.attrType == 'float') {
value.properties[currentSelect].min = 1;
let rules = [
{ required: true, message: '不能为空!' },
{
pattern: '^[0-9]{0,9}[.][0-9]{1,9}$',
message: '只能输入float类型!',
},
];
value.properties[currentSelect].rules = rules;
value.properties[currentSelect].required = true;
} else if (m.attrType == 'int') {
value.properties[currentSelect].min = 1;
value.properties[currentSelect].max = 5;
let rules = [
{ required: true, message: '不能为空!' },
{
pattern: '^[0-9]{0,5}$',
message: '只能输入int类型!',
},
];
value.properties[currentSelect].rules = rules;
value.properties[currentSelect].required = true;
}
setMySchema(value);
}
});
}
actionSettting(useProperty);
}
}
}
};
const onCanvasSelect = (schema) => {
// 获取当前选中的组件唯一ID
// console.log(schema.$id?.split('/')[1], 'onCanvasSelect', schema);
setCurrentSelect(schema.$id?.split('/')[1]);
};
// 封装公共的方法,根据全部属性和使用属性,找出剩余的属性并重新设置
const actionSettting = (useProperty) => {
let transProperList = JSON.parse(JSON.stringify(properList)).filter(
(m) => !useProperty.includes(m.id),
);
let transEnum = transProperList.map((m) => m.id);
let transEnumName = transProperList.map((m) => m.attrDescribe);
setFormSetting({
weightProperty: {
title: '关联数据表',
type: 'string',
enum: transEnum,
enumNames: transEnumName,
weight: 'select',
},
...commonSettings,
});
};
// 删除操作,因为在删除时,表单的onSchemaChange方法setFormSetting时页面不会重新渲染,只能在删除内操作
const canDelete = () => {
let transSchema = JSON.parse(JSON.stringify(mySchema));
if (transSchema) {
let useProperty = [];
for (let key in transSchema.properties) {
let target = transSchema.properties[key];
// 因为这里是删除操作,当选中的组件id和当前删除的id不一致时,才可以添加
if (
target.weightProperty &&
target.weightProperty !=
transSchema.properties[currentSelect]?.weightProperty
) {
useProperty.push(target.weightProperty);
}
}
actionSettting(useProperty);
}
return true;
};
const columns = [
{
title: '序号',
dataIndex: 'order',
key: 'order',
ellipsis: true,
},
{
title: '控件标题',
dataIndex: 'title',
key: 'title',
ellipsis: true,
},
{
title: '关联数据表名',
dataIndex: 'titleProperty',
key: 'titleProperty',
ellipsis: true,
},
];
// 保存、发布前需要处理schema数据,设置弹窗列表数据源
const actionClick = (val) => {
// 根据输出的schema 取出table信息
let value = genRef.current && genRef.current.getValue();
let transProperList = JSON.parse(JSON.stringify(properList));
let transData = [];
let num = 1;
for (let key in value.properties) {
let target = value.properties[key];
// console.log(key, value.properties[key]);
// 展示列表前进行校验,关联表是否选择
if (!target?.weightProperty) {
message.error('请选择关联表');
return false;
}
let middleData = {};
middleData.order = num;
middleData.title = transProperList.filter(
(m) => m.id == target.weightProperty,
)[0].attrDescribe;
middleData.titleProperty = target.formId;
middleData.weightProperty = target.weightProperty;
transData.push(middleData);
num++;
}
// console.log(transData, 'transData');
setTableData(transData);
setStatus(val);
setActionTip(true);
};
// 保存、发布按钮
const modalAction = () => {
// status === 'save' ? '保存' : '发布'
let issue = status === 'save' ? 0 : 1;
let schema = genRef.current && genRef.current.getValue();
dispatch({
type: 'userSystem/saveFormDesign',
payload: {
formId: selectValue,
properties: tableData.map((m) => m.weightProperty),
issue: issue,
schema: JSON.stringify(schema),
},
}).then((res) => {
let tip = status === 'save' ? '保存' : '发布';
if (res && res.status === 20000) {
message
.success(`${tip}成功`)
.then(() => history.push('/designCenter/formManage'));
} else {
message.error(`${tip}失败`);
}
setActionTip(false);
});
};
// 退出按钮
const outClick = (val) => {
if (val === 'modalOk') {
// console.log('保存并退出');
// 退出前进行校验,关联表是否选择
let value = genRef.current && genRef.current.getValue();
for (let key in value.properties) {
let target = value.properties[key];
if (!target?.weightProperty) {
message.error('请选择关联表');
return false;
}
}
dispatch({
type: 'userSystem/saveFormDesign',
payload: {
formId: selectValue,
properties: tableData.map((m) => m.weightProperty),
issue: 0,
schema: JSON.stringify(value),
},
}).then((res) => {
if (res && res.status === 20000) {
message
.success('保存成功')
.then(() => history.push('/designCenter/formManage'));
} else {
message.error('保存失败');
}
setActionTip(false);
});
} else if (val === 'modalOut') {
// console.log('不保存');
history.push('/designCenter/formManage');
} else {
history.push('/designCenter/formManage');
// console.log('取消');
}
setOutTip(false);
};
// 退出按钮
const backOut = () => {
setOutTip(true);
};
return (
<div className={styles.formDesing}>
<div className={styles.topContent}>
<div style={{ fontSize: '20px' }}>编辑表单</div>
<div className={styles.rightContent}>
<Button type="primary" onClick={() => actionClick('save')}>
保存
</Button>
<Button type="primary" onClick={() => actionClick('publish')}>
发布
</Button>
<ExportOutlined
style={{ fontSize: '20px', color: '#00B4ED', cursor: 'pointer' }}
onClick={backOut}
/>
</div>
</div>
<div className={styles.selectContent}>
<span>数据模型:</span>
<Select
allowClear
style={{ width: '200px' }}
value={selectValue}
onChange={selectChange}
>
{Array.isArray(unIssueList) &&
unIssueList.length > 0 &&
unIssueList.map((item, index) => {
return (
<Select.Option value={item.id} key={index}>
{item.formName}
</Select.Option>
);
})}
</Select>
</div>
<Spin spinning={loading} indicator={<span>请选择关联数据模型!</span>}>
<div style={{ height: '80vh' }}>
<Generator
ref={genRef}
onCanvasSelect={onCanvasSelect} // 当选中的画布组件变化时
onSchemaChange={onSchemaChange} // 当表单schema变化时
extraButtons={[true, true, false, true]} // 前四项为插件默认按钮是否显示,之后才可以配置自定义按钮
controlButtons={[true, false]} // 前两项为插件默认按钮是否显示,之后才可以配置自定义按钮
canDelete={canDelete} // 删除事件
hideId={true} // 不展示默认ID
commonSettings={formSetting} // 右侧组件通用配置
settings={settings} // 左侧基础组件配置
globalSettings={globalSettings} // 右侧表单配置
/>
</div>
</Spin>
<Modal
title="信息提示"
open={outTip}
onCancel={() => setOutTip(false)}
footer={[
<Button
key="modalOk"
type="primary"
onClick={() => outClick('modalOk')}
>
保存并退出
</Button>,
<Button
key="modalOut"
type="primary"
onClick={() => outClick('modalOut')}
>
不保存
</Button>,
<Button key="modalCancel" onClick={() => outClick('modalCancel')}>
取消
</Button>,
]}
>
<div style={{ fontSize: '16px', margin: '10px' }}>
<ExclamationCircleOutlined
style={{ color: '#FFA96D', marginRight: '6px' }}
/>
<span>表单数据暂未保存</span>
</div>
<div style={{ marginLeft: '10px' }}>请确认是否退出</div>
</Modal>
<Modal
title={status === 'save' ? '保存表单' : '发布表单'}
width="800px"
open={actionTip}
onCancel={() => setActionTip(false)}
footer={[
<Button key="modalAction" type="primary" onClick={modalAction}>
{status === 'save' ? '保存' : '发布'}
</Button>,
<Button
key="cancel"
type="primary"
onClick={() => setActionTip(false)}
>
取消
</Button>,
]}
>
<div style={{ fontSize: '14px', margin: '10px' }}>
<div style={{ marginBottom: '20px' }}>
<span>表单名称:</span>
<span>test1</span>
</div>
<div style={{ marginBottom: '20px' }}>
<span>关联数据模型:</span>
<span>test_table</span>
</div>
<Table columns={columns} dataSource={tableData} pagination={false} />
</div>
</Modal>
</div>
);
};
export default UserSystem;