公司項目中挺多用到Antd表單和列表的携冤,自己封裝感覺api太多總是封裝得不夠好盒刚,恰好看到Antd這邊有自己的封裝细诸,用了一下發(fā)現真的很方便惕它,在此記錄一下使用心得怕午。
基礎表單
類似這樣的表單頁,用一個BetaSchemaForm組件就能實現,配置表單項和前后端數據轉換都可以在一個配置對象里搞定淹魄,能剩下很多時間郁惜,代碼更加簡潔
import { createElement } from "react";
import type {
ProFormColumnsType,
} from '@ant-design/pro-components';
import {ArrowRightOutlined,MinusCircleOutlined,PlusCircleOutlined} from '@ant-design/icons';
import { Button } from "antd";
import { queryTaskNameListUsingPOST } from "@/services/cluster/IServerDataMigrationAPI";
import to from "await-to-js";
import { validationTypeMap,validationEMap,validationFMap } from "../../config";
type DataItem = {
name: string;
state: string;
};
export default function getColumn(info:any){
return [
{
title:'是否編輯',
dataIndex:'isEdit',
formItemProps:{
hidden:true
},
initialValue:false
},
{
title:'',
dataIndex:'id',
formItemProps:{
hidden:true
},
initialValue:undefined
},
{
title:'數據校驗任務名稱',
dataIndex:'validationName',
colProps: {
span:12
},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
}
},
{
title: '數據遷移任務',
dataIndex: 'taskName',
valueType:'select',
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
colProps: {
span:12,
},
fieldProps:{
labelInValue:true
},
params: {clusterId:info.ClusterId},
async request(){
const [err,res] = await to(queryTaskNameListUsingPOST({
clusterId:info.ClusterId
}))
if (res?.Message === 'Success' && res?.Code === '00000') {
// message.success('提交成功')
return res.Data?.map?.(d=>({label:d.taskName,value:d.taskId}))
} else {
// message.error(res?.Message || String(err))
return []
}
}
},
{
title: '數據校驗任務描述',
dataIndex: 'validationDesc',
valueType: 'textarea',
colProps: {
span:24
},
fieldProps:{
// showCount:true,
rows:3
}
},
{
title: '校驗類型',
dataIndex: 'validationType',
valueType: 'select',
initialValue:'QUICK_COUNT',
valueEnum:validationTypeMap,
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項'
}
]
},
colProps: {
span:12
},
fieldProps:{
// showCount:true,
rows:1
}
},
{
title: '校驗頻次',
dataIndex: 'validationFrequency',
valueType: 'select',
valueEnum:validationFMap,
initialValue:'SINGLE',
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
}
]
},
colProps: {
span:12
},
fieldProps:{
// showCount:true,
rows:1
}
},
{
title: '錯誤數據保存條數',
dataIndex: 'validationKeep',
valueType: 'select',
initialValue:'ONE_HUNDRED',
valueEnum:validationEMap,
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
colProps: {
span:12
},
fieldProps:{
// showCount:true,
rows:1
}
},
// 大標題
{
title: '',
colProps:{
span:24
},
formItemProps:{
noStyle:true
},
// rowProps:{gutter:48},
renderFormItem:()=>
createElement(
'h4',
null,
[
createElement('div',{className:'check-condition'}),
'校驗條件'
]
)
},
{
valueType:'dependency',
name:['taskName'],
columns({taskName}) {
return [
{
title: '待校驗表',
valueType: 'formList',
dataIndex: 'tables',
initialValue:[{}],
fieldProps:{
min:1,
copyIconProps:false,
deleteIconProps:{
// Icon:createElement(MinusCircleOutlined,{style:{color:'#ff8'}})
Icon:(props:any)=>createElement(MinusCircleOutlined,{style:{color:'#FF4D4F'},...props})
},
creatorButtonProps:{
creatorButtonText:'手動添加待校驗表',
icon:createElement(PlusCircleOutlined),
type:'link',
block:false,
},
},
formItemProps:{
rules: [
{
required: true,
message: '此項為必填項',
},
]
},
colProps: {
span:24
},
columns:[
{
valueType: 'group',
columns: [
{
valueType:'dependency',
name:['tables'],
fieldProps: {
ignoreFormListField:true
},
columns({tables}) {
return [
{
title: '',
dataIndex: 'sourceTablename',
valueType: 'select',
colProps: {
flex:'438px'
},
params:{taskName,tables},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
debounceTime:100,
request(){
return info.tableMapping?.map?.(d=>({
label:d.sourceTablename,
value:d.sourceTablename,
disabled:tables.map(t=>t.sourceTablename).includes(d.sourceTablename)
})) || []
},
},
]
},
},
{
title: '',
valueType: 'text',
colProps: {
flex:'31px'
},
renderFormItem(){
return createElement('div',{className:'rarrow-icon'},[createElement(ArrowRightOutlined)])
}
},
{
valueType:'dependency',
name:['tables','sourceTablename'],
columns({tables,sourceTablename}) {
// console.log(sourceTablename,'sourceTablename',info.tableMapping,'info.tableMapping',info.tableMapping.find?.(t=>t.sourceTablename === sourceTablename)?.destTablename)
return [
{
title: '',
dataIndex: 'destTablename',
valueType: 'select',
colProps: {
flex:'438px'
},
params:{taskName,tables},
// formItemProps:{
// readOnly:true
// },
fieldProps:{
disabled:true,
value:info.tableMapping.find?.(t=>t.sourceTablename === sourceTablename)?.destTablename,
},
debounceTime:100,
request(){
return info.tableMapping?.map?.(d=>({
label:d.destTablename,
value:d.destTablename
}))
}
},
]
},
},
],
}]
}
]
},
},
{
title: '',
formItemProps:{
style:{
height:0,
margin:0
}
},
renderFormItem:(schema,config,form)=>
createElement(
Button,
{
type:'link',
icon:createElement(PlusCircleOutlined),
className:'autoadd-button',
onClick(){
form.setFieldValue('tables',info.tableMapping.length? info.tableMapping: [{}])
}
},
'自動添加待校驗表'
)
},
] as ProFormColumnsType<DataItem>[]
}
// tsx 文件
<BetaSchemaForm<DataItem>
className="datav-form"
layoutType="Form"
grid
shouldUpdate
rowProps={{gutter:24}}
formRef={formRef}
// onFinish={handleFinish}
columns={formColumn({ClusterId,tableMapping:formRef.current?.getFieldValue('taskName')?tableMapping:[]})}
submitter={{render:false}}
onValuesChange={changeValues=>{
// console.log(changeValues,'changeValues')
Object.keys(changeValues).map(k=>{
if (k === 'taskName') {
formRef.current?.setFieldValue('tables',[{}])
getTableMapping(changeValues[k])
}
})
}}
/>
分步表單
分步表單會復雜一些,但是框架也提供了StepsForm甲锡,用法與BaseForm一致兆蕉,只要稍微注意一下配置對象就能完美搭建一個分布表單的頁面
// index.tsx
import { Space,message,Button,Layout,Col,Row,ConfigProvider,Popover, Steps } from 'antd';
import React,{useRef,useEffect,useState} from 'react';
import formColumn from "./formColumn";
import { CloseOutlined } from "@ant-design/icons";
import './index.less';
import moment from "moment";
import { useModel } from "umi";
import type {
FormInstance,
ProFormColumnsType,
} from '@ant-design/pro-components';
import { BetaSchemaForm } from '@ant-design/pro-components';
import to from "await-to-js";
import { history,useLocation } from "umi";
import { isEmpty } from "lodash";
import { createOrEditTaskUsingPOST } from "@/services/cluster/IServerDataMigrationAPI";
import type { ProFormInstance } from '@ant-design/pro-components';
import CryptoJS from 'crypto-js';
import { encrypt } from '@/utils/utils';
export type SettingDrawerProps = {
visible: boolean;
info:any
onCancel: () => any;
onOk: () => any;
};
type DataItem = {
name: string;
state: string;
};
const NewTask: React.FC<SettingDrawerProps> = (props:SettingDrawerProps) => {
// const {visible,onCancel,info} = props
const [current, setCurrent] = useState(0)
const formMapRef = useRef<
React.MutableRefObject<ProFormInstance<any> | undefined>[]
>([]);
const { ClusterId } = useModel('clusterModel', (ret: { clusterId: any }) => ({
ClusterId: ret.clusterId,
}));
const { setFormData,setStep } = useModel('jformModel');
const [tableLoading, setTableLoading] = useState(false)
const location = useLocation()
const isEdit = location?.state?.isEdit
const record = location?.state?.record
useEffect(()=>{
// console.log(location?.state?.record)
if (!isEmpty(record) && isEdit) {
// console.log(record,record?.destTables,'record?.destTables')
formMapRef?.current?.forEach((formInstanceRef,i) => {
if(i === 1){
formInstanceRef?.current?.setFieldsValue({
destTables:isEdit?(record?.destTables?.split(',') || []):[],
sourceTables:isEdit?(record?.sourceTables?.split(',') || []):[],
isEdit
})
} else {
formInstanceRef?.current?.setFieldsValue({
...record,
isEdit
})
if (i === 2) {
if (moment(record.readStartCommit).isValid()) {
setTimeout(() => {
formInstanceRef?.current?.setFieldsValue({
readStartCommit:'2',
diyDate:moment(record.readStartCommit)
})
})
}
}
}
})
}
},[record,isEdit])
// 設置是否編輯狀態(tài)
// useEffect(()=>{
// formMapRef?.current?.forEach((formInstanceRef,i) => {
// formInstanceRef?.current?.setFieldValue('isEdit',isEdit)
// })
// },[isEdit])
//控制提交
async function handleFinish(val:any) {
// const myKey = 'xB2]dZ2?pY0&rK5['
//AES加密createOrEditTaskUsingPOST
const params = {
...val,
clusterId:ClusterId,
sourceTables:val.destTables,
sourceNamenodes:val.sourceNamenodes?.split(','),
sourceNamenodeRpcAddress:val.sourceNamenodeRpcAddress?.split(',')
// id:record.id
}
if (isEdit) {
params.id = record.id
}
if (val.readStartCommit === '2') {
params.readStartCommit = val.diyDate
}
// console.log(params,'handleFinish');
const [err,res] = await to(createOrEditTaskUsingPOST(params))
if (res?.Message === 'Success' && res?.Code === '00000') {
message.success('提交成功');
} else {
message.error(res?.Message || String(err))
}
formMapRef?.current?.forEach((formInstanceRef) => {
formInstanceRef?.current?.resetFields()
});
setFormData(undefined)
setStep(0)
history.replace('/migration/tasks')
}
// 下一步或者一步回調
function onCurrentChange(current:number) {
// console.log('====================================');
// console.log('onCurrentChange',current,formRef.current?.validateFieldsReturnFormatValue());
// console.log('onCurrentChange',current,formRef.current?.getFieldsValue());
// console.log('====================================');
setCurrent(current)
setStep(current)
}
return (
<div className='new-task-block'>
<div className='new-task'>
{/* <section className='title'>新建遷移任務<CloseOutlined className='icon-close' onClick={()=>history.replace('/migration/tasks')}/></section> */}
<BetaSchemaForm<DataItem>
layoutType="StepsForm"
width="100%"
steps={[
{
title: '選擇數據源和目標庫',
name:'base',
rowProps:{
gutter: 48
},
onFinish(values){
setFormData(values)
return true
}
},
{
title: '選擇遷移對象',
name:'tableShow',
rowProps:{
gutter: 48
}
},
{
title: '配置任務',
name:'task',
rowProps:{
gutter: 48
}
}
]}
onCurrentChange={onCurrentChange}
stepsProps={{size:'small',direction:"vertical",progressDot:true}}
grid
stepFormRender={form =>
<div className='steps-form-wrap'>
<div className='steps-form'>
{form}
{!current && <div className='long-div'></div>}
</div>
</div>
}
stepsFormRender={(form,submitter)=><div className='form-wrap-index'>{[form,submitter]}</div>}
stepsRender={(steps,dom)=>
<div className='steps-container'>
<div className='steps-title'>{`${isEdit?'編輯':'新建'}數據遷移任務`}</div>
<ConfigProvider theme={{
token:{
colorText:'#fff',
colorTextDisabled:'#CCC',
colorTextDescription:'#CCC',
colorSplit:'#fff'
}
}}>
{dom}
</ConfigProvider>
</div>
}
formMapRef={formMapRef}
onFinish={handleFinish}
columns={formColumn(tableLoading,setTableLoading) as ProFormColumnsType<DataItem>[][]}
submitter={{
submitButtonProps:{
// disabled:current === 1 && !formMapRef.current?.[1].current?.getFieldValue('destTables')?.length
disabled:tableLoading
},
render:(props,dom)=>
<div className='submitter'>
<Space>
<Button onClick={()=>{setStep(0);setFormData({});history.replace('/migration/tasks')}}>取消</Button>
{dom}
</Space>
</div>
}}
/>
</div>
</div>
)
}
export default NewTask;
// 配置項
import type { ProFormColumnsType } from '@ant-design/pro-components';
import { DatePicker, Form, Radio } from 'antd';
import { createElement } from 'react';
import TableSelect from './TableSelect';
// import { Cron } from "react-js-cron";
import { existsUsingPOST } from '@/services/cluster/IServerDataMigrationAPI';
import { decrypt, encrypt } from '@/utils/utils';
import CronInput from './CronInput';
type DataItem = {
name: string;
state: string;
};
const valueEnum = {
all: { text: '全部', status: 'Default' },
open: {
text: '未解決',
status: 'Error',
},
closed: {
text: '已解決',
status: 'Success',
disabled: true,
},
processing: {
text: '解決中',
status: 'Processing',
},
};
export default (tableLoading: boolean, setTableLoading: (tableLoading: boolean) => void) =>
[
[
{
title: '是否編輯',
dataIndex: 'isEdit',
formItemProps: {
hidden: true,
},
},
{
title: '標題',
dataIndex: 'taskName',
colProps: {
span: 12,
// offset:1
},
formItemProps: (form, config) => ({
rules: [
{
required: true,
// validateTrigger:'onBlur',
validator: (_, value, callback) => {
if (!value) return callback('請輸入');
setTimeout(() => {
existsUsingPOST({
// sourceCatalog:form.getFieldValue('sourceCatalog'),
sourceCatalog: '',
taskName: value,
})
.then((res) => {
if (res?.Message === 'Success' && res?.Code === '00000') {
if (res.Data?.Data && !form.getFieldValue('isEdit')) {
// return Promise.reject('已存在相同名稱的任務名')
callback('已存在相同名稱的任務名');
} else {
callback();
}
// return Promise.resolve()
} else {
// return Promise.reject(res?.Message || '已存在相同名稱的任務名')
callback('已存在相同名稱的任務名');
}
})
.catch((err) => err.Message || '已存在相同名稱的任務名');
});
},
},
],
}),
},
{
title: '任務描述',
dataIndex: 'taskDesc',
valueType: 'textarea',
valueEnum,
colProps: {
span: 12,
},
fieldProps: {
// showCount:true,
rows: 1,
},
},
{
title: '',
dataIndex: 'ff',
colProps: {
span: 12,
},
// rowProps:{gutter:48},
renderFormItem: () => createElement('div', { className: 'form-item-box' }, '數據源'),
},
{
title: '',
dataIndex: 'f1',
colProps: {
span: 12,
},
// rowProps:{gutter:48},
renderFormItem: () => createElement('div', { className: 'form-item-box' }, '目標庫'),
},
{
title: '數據源catalog名稱',
dataIndex: 'sourceCatalog',
colProps: {
span: 12,
},
formItemProps: (form, config) => ({
rules: [
{
required: true,
// validateTrigger:'onBlur',
validator: (_, value, callback) => {
if (!value) return callback('請輸入');
setTimeout(() => {
existsUsingPOST({
sourceCatalog: value,
taskName: '',
})
.then((res) => {
if (res?.Message === 'Success' && res?.Code === '00000') {
if (res.Data?.Data && !form.getFieldValue('isEdit')) {
// return Promise.reject('已存在相同名稱的任務名')
callback('已存在相同名稱的數據源catalog名稱');
} else {
callback();
}
// return Promise.resolve()
} else {
// return Promise.reject(res?.Message || '已存在相同名稱的任務名')
callback('已存在相同名稱的數據源catalog名稱');
}
})
.catch((err) => callback(err));
});
},
},
],
}),
},
{
title: '目標庫catalog名稱',
dataIndex: 'destCatalog',
colProps: {
span: 12,
},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
{
title: '數據源database名稱',
dataIndex: 'sourceDatabase',
colProps: {
span: 12,
},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
{
title: '目標庫database名稱',
dataIndex: 'destDatabase',
colProps: {
span: 12,
},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
// {
// title: '數據源URL',
// dataIndex: 'sourceUrl',
// colProps: {
// span:12
// },
// formItemProps: {
// rules: [
// {
// required: true,
// message: '此項為必填項',
// },
// ],
// },
// },
{
title: 'hive配置路徑',
dataIndex: 'sourceHiveConfPath',
colProps: {
span: 12,
},
// width:'502px',
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
{
title: '目標庫URL',
dataIndex: 'destUrl',
colProps: {
span: 12,
},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
// {
// title: '數據源賬號',
// dataIndex: 'sourceAccount',
// colProps: {
// span:12
// },
// formItemProps: {
// rules: [
// {
// required: true,
// message: '此項為必填項',
// },
// ],
// },
// },
// {
// title: '數據源密碼',
// dataIndex: 'sourcePassword',
// valueType:'password',
// colProps: {
// span:12
// },
// formItemProps: {
// rules: [
// {
// required: true,
// message: '此項為必填項',
// },
// ],
// },
// },
{
title: 'hadoop命名服務',
dataIndex: 'sourceNameservices',
colProps: {
span: 12,
},
// width:'502px',
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
{
title: '目標庫賬號',
dataIndex: 'destAccount',
colProps: {
span: 12,
},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
{
title: '指定命名服務的高可用節(jié)點列表',
dataIndex: 'sourceNamenodes',
colProps: {
span: 12,
},
// width:'502px',
// transform:(value,namePath,allValues)=> {
// const a = value?.split?.(",")
// console.log(a);
// return a
// },
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
{
title: '目標庫密碼',
dataIndex: 'destPassword',
valueType: 'password',
colProps: {
span: 12,
},
transform(value) {
const res = decrypt(value);
return {
destPassword: res ? value : encrypt(value),
};
},
convertValue(convertValue) {
// console.log(convertValue);
const res = decrypt(convertValue);
return res || convertValue;
},
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
{
title: '指定命名服務中每個節(jié)點的RPC地址列表',
dataIndex: 'sourceNamenodeRpcAddress',
colProps: {
span: 12,
},
// width:'502px',
// transform:(value,namePath,allValues)=> value?.split?.(","),
formItemProps: {
rules: [
{
required: true,
message: '此項為必填項',
},
],
},
},
],
[
{
title: '是否編輯',
dataIndex: 'isEdit',
formItemProps: {
hidden: true,
},
},
{
title: '原表',
dataIndex: 'sourceTables',
formItemProps: {
hidden: true,
},
// convertValue(value) {
// return value?.split?.(',') || [];
// },
},
{
title: '',
dataIndex: 'destTables',
valueType: 'text',
formItemProps: {
rules: [
{
required: true,
message: '請選擇需要遷移的源表',
},
],
},
// convertValue(value) {
// return value?.split?.(',') || [];
// },
renderFormItem: (schema, config, form) =>
createElement(TableSelect, { form, tableLoading, setTableLoading }),
},
],
[
{
title: '是否實時',
valueType: 'radio',
dataIndex: 'readStreamingEnabled',
colProps: {
span: 12,
},
initialValue: 'TRUE',
formItemProps: {
rules: [
{
required: true,
message: '請選擇',
},
],
},
fieldProps: {
options: [
{
label: '是',
value: 'TRUE',
},
{
label: '否',
value: 'FALSE',
},
],
},
},
{
valueType: 'dependency',
name: ['readStreamingEnabled'],
columns: ({ readStreamingEnabled }) => {
return readStreamingEnabled === 'TRUE'
? [
{
title: '設置起始提交',
dataIndex: 'readStartCommit',
valueType: 'text',
initialValue: 'latest',
formItemProps: {
rules: [
{
required: true,
message: '請選擇',
},
],
},
renderFormItem(schema, config, form) {
return createElement(Radio.Group, { name: 'readStartCommit' }, [
createElement(Radio, { value: 'earliest', key: 'earliest' }, 'earliest'),
createElement(Radio, { value: 'latest', key: 'latest' }, 'latest'),
createElement(Radio, { value: '2', key: '2' }, [
'自定義時間',
form.getFieldValue('readStartCommit') === '2'
? createElement(
Form.Item,
{
noStyle: true,
name: 'diyDate',
rules: [
{
required: form.getFieldValue('readStartCommit') === '2',
message: '請確定起始提交時間',
},
],
},
createElement(DatePicker, {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
className: 'diy-date',
}),
)
: null,
]),
]);
},
},
]
: [];
},
},
{
valueType: 'dependency',
name: ['readStreamingEnabled'],
columns: ({ readStreamingEnabled }) => {
return readStreamingEnabled === 'FALSE'
? [
{
title: '模式',
dataIndex: 'mode',
valueType: 'radio',
initialValue: 'INCREMENTAL',
formItemProps: {
rules: [
{
required: true,
message: '請選擇',
},
],
},
fieldProps: {
options: [
{
label: '增量',
value: 'INCREMENTAL',
},
{
label: '全量',
value: 'FULL',
},
],
},
},
]
: [];
},
},
{
valueType: 'dependency',
name: ['readStreamingEnabled'],
columns: ({ readStreamingEnabled }) => {
return readStreamingEnabled === 'FALSE'
? [
{
title: 'cron表達式',
dataIndex: 'cron',
valueType: 'text',
colProps: {
span: 12,
},
initialValue: '* * * * * ? *',
formItemProps: {
rules: [
{
required: true,
message: '請選擇',
},
],
},
renderFormItem(schema, config, form) {
return createElement(CronInput);
},
},
]
: [];
},
},
{
title: '表的合并任務數量',
dataIndex: 'compactionTasks',
valueType: 'digit',
initialValue: 1,
width: '160px',
formItemProps: {
rules: [
{
required: true,
message: '請選擇',
},
],
},
},
{
title: '表寫入操作的并行任務數',
dataIndex: 'writeTasks',
valueType: 'digit',
initialValue: 1,
width: '160px',
formItemProps: {
rules: [
{
required: true,
message: '請選擇',
},
],
},
},
],
] as ProFormColumnsType<DataItem>[][];
值得一提的是改表單頁面需要用到grid布局.這里貼一下less文件
.new-task-block{
overflow-x: hidden;
height: calc(100vh - 54px);
width: 100%;
}
.new-task{
position: relative;
height: 100%;
.ant-pro-steps-form{
height: 100%;
}
}
.form-wrap-index{
height: 100%;
div:first-of-type.ant-row.ant-row-stretch{
height: 100%;
display: grid;
grid-template-columns: 256px auto;
grid-template-rows: auto;
}
.ant-pro-steps-form-container{
width: 100%;
min-width: 100%;
height: 100%;
overflow-y: auto;
padding-bottom: 80px;
}
}