zl程序教程

您现在的位置是:首页 >  其它

当前栏目

Ant Design在处理业务表单中的一些实践

处理 实践 一些 业务 表单 Design ant
2023-09-11 14:21:23 时间

前言

目前,很多中小企业前端实现采用了react+antd技术栈。在传统业务中,大部分后台的表单和列表模式类似,通过antd组件快速搭建,可以节约时间成本。本文是实际场景中的一些实践记录,供大家参考。

表单处理实践

Modal 清空旧数据

组件有标准的 React 生命周期,关闭后状态不会自动清空。 如果希望每次打开都是新内容,需要自行手动清空旧的状态。或者打开时给 Modal 设置一个全新的 key, React 会渲染出一个全新的对话框。

第一种清空方式:显示弹窗时设置一个新的key

 componentWillReceiveProps(nextprops){
    //打开时设置一个新key
    if(nextprops.visible)
       this.setState({newKey:!this.state.newKey})
  }
  <Modal key={this.state.newKey} visible={this.props.visible} />

第二种清空方式:关闭弹窗后,清空弹窗数据,弹窗内是表单时重置表单。

afterClose = () => {
     // 隐藏动画完成后重置内容
     this.props.form.resetFields();
 }
 <Modal afterClose={this.afterClose} visible={this.props.visible} />

使用Form.create和getFieldDecorator对Form进行包装

经过包装的Form具备以下特点:

使用 Form.create 处理后的表单具有自动收集数据并校验的功能;
不再需要用 onChange 来做同步,但还是可以继续监听 onChange 等事件;
不能用控件的 value或defaultValue 等属性来设置表单域的值,默认值可以用 getFieldDecorator 里的 initialValue;
不需要用 setState,可以使用 this.props.form.setFieldsValue 来动态改变表单值,使用 this.props.form.getFieldsValue来获取表单值。
包装一个表单包括三个步骤:
第一步:

class CustomizedForm extends React.Component {}

CustomizedForm = Form.create({})(CustomizedForm);

第二步:使用getFieldDecorator装饰对应的field

const FormItem = Form.Item;
const formItemLayout = {
      labelCol: { span: 5 },
      wrapperCol: { span: 12 }
};
<Form onSubmit={this.submit}>
    <FormItem {...formItemLayout} label={'名称'}>
        {getFieldDecorator('fieldName', {
            //rules是表单的校验规则
            rules: [{ required: true, message: '请填写表单字段!' }],
            initialValue:'初始值'
          })(
            <Input />
          )}
    </FormItem>
</Form>

第三步:submit时添加校验逻辑和后处理

submit = (e)=> {
    //这里去掉默认行为,可以避免用户操作回车时提交表单
    e.preventDefault();
    //校验表单的值
    this.props.form.validateFields((err, values) => {
      if (!err) {
        //后处理操作
      }
   }
}

表单控件是switch

switch开关的值参数名是checked而不是value,使用时有两种方式:
第一种:引入团队封装后的switch控件

tnpm install @ali/uniform-react-components --save
import Switch from ‘@ali/uniform-react-components/lib/Switch/index’;
第二种: 使用valuePropName

<FormItem {…formItemLayout} label=“Switch” > {getFieldDecorator(‘switch’, { valuePropName: ‘checked’ })( )}

自定义表单控件

自定义表单控件的需求场景很多,为了能够支持 antd Form 的 getFieldDecorator 包装,从而使组件能直接用于 Form 中,需要遵守以下规则:

提供受控属性 value 或其它与 valuePropName 的值同名的属性。
提供 onChange 事件或 trigger 的值同名的事件。
不能是函数式组件。
大CMS业务中用到的自定义表单控件有很多,例如UploadImg,TreeSelect,BizTypeSelect等等,具体可以参考youku-manager和youku-scg等工程里面的实践。

Table列表

第一步:定义columns 即列表中列的描述

columns = [{
    title: 'ID',
    dataIndex: 'id',
    key: 'id'
  }, {
    title: '内容数量',
    key: 'amount',
    //render用于生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据
    render: (text,record) => <a onClick={()=>this.relationTagPopup(record)}>查看</a>,
  },{
    title: '操作',
    key: 'action',
    //删除,下线等post操作使用PopConfirm进行二次确认
    render: (item) => (
      <div>
        <a onClick={() => this.editTag(item)}>编辑</a>
        <span className='ant-divider'></span>
        <Popconfirm title="确定删除吗?" onConfirm={()=>this.deleteTag(item)}>
           <a>删除</a>
        </Popconfirm>
      </div>
    )
  }];

第二步:定义Table
rowKey属性是表格行 key 的取值,这里取的是column中的id字段,保证key值不重复
loading表示页面是否加载中,在翻页和查询时添加load效果,可以优化体验
dataSource 数据数组

<Table columns={this.columns} loading={state.loading} dataSource={state.tagList} pagination={{ pageSize:10, total: state.total, current: state.pageNo, }} onChange={this.handlePagination} rowKey="id" />

第三步:更新dataSource数据源

this.setState({loading:true})
IO.getTagRepertoryList(data)
      .then(response => {
        if(response.success){
          const data = response.data;
          const tagList = data.itemList;
          const total = data.totalSize;
          const pageNo =  data.pageNo;
          const loading = false;
          this.setState({tagList,pageNo,total,loading});
        }
        else{
          Message.warning(response.message)
          this.setState({
            tagList:[],
            total:0,
            pageNo:1,
            loading:false
          });
        }
      })
      .catch(err => {
        console.error(err);
         this.setState({
           loading:false,
           tagList:[],
           total:0,
           pageNo:1,
         });
         Message.error('查询异常,无结果');
      });

Select

cms后台的业务有很多选择选择下拉需求,下面两个业务常见的select为例,解释几个比较重要的配置

多值远程搜索
screenshot.png

screenshot.png

render() { 
//fetching 查询状态,用于在接口数据返回前显示loading效果 
const { fetching, personList } = this.state; 
// Option的name属性对应的是optionLabelProp设置的值 
const options = personList.map((d,index)=> 
    <Option key={index} value={(d.id).toString()} 
    name={d.name}> 
    <div className='person-item-img'><img src={d.thumbUrl} />
    </div> 
	<span className='person-item'>{d.name}</span> 
	<span className='person-item'>{d.birthday}</span> 
</Option>) 
return ( <div className='person-search'> 
	<Select defaultValue={this.props.defaultVal} 
	       mode="multiple" placeholder="输入相关人物名称检索选择" 
	       notFoundContent={fetching ? <Spin size="small" /> : null} 
	       optionLabelProp='name' filterOption={false} 
	       onSearch={this.fetchUser} 
	       onChange={this.handleChange} 
	       onFocus={this.fetchUser} style={{ width: '100%' }} 
	       labelInValue > 
	    {options} 
	</Select> 
	</div> 
) 

optionLabelProp和labelInValue配合,默认情况下 onChange 里只能拿到 value,如果需要拿到选中的节点文本 label,可以使用 labelInValue 属性,这个人物搜索组件的Option的children是ReactNode,所以我们需要通过optionLabelProp拿到需要的文本,回显在input框中,同时给后端传递所需要的map结构。

filterOption是否根据输入项进行筛选,如果这个select的option是通过用户输入关键字请求接口渲染的,那么需要设置成false,否则可以通过特定函数设定内部筛选规则,具体的使用方法在第二个例子中具体说明。

单值内部搜索
screenshot.png

<Select className='select-feature'
              showSearch
              filterOption={(input, option) => {
                return (input === option.props.value || option.props.name.indexOf(input)>-1)
              }}
              value={props.id+''}
              onChange={this.handleChange}
            >
              {this.featureOptions}
 </Select>

showSearch 用于在选择框中显示搜索框

filterOption 在这里定义内部搜索逻辑,默认是通过option的value进行匹配,这里为了能既能够通过id也能够通过name进行搜索,通过这个函数进行了特殊设置。

使用Fetch进行接口调用

import {fetchGet, fetchPost, fetchJsonp} from '@ali/uniform-react-components/lib/UniFetch/index';
//大cms后台的post请求需要增加token校验 
const _tb_token_ = window._tb_token_;
const params = {
  //fetch 请求默认是不带 cookie 的
  credentials: 'include',
  //是否允许跨域
  //mode: "no-cors",
  headers: {
  //服务端通过识别X-Requested-With来判定发送的fetch请求是ajax请求,数据统一封装成Json返回
    "X-Requested-With":"XMLHttpRequest"
  },
}
class IO {
  /** * get 请求例子 */
  static fetchGetSample(data) {
    return fetchGet('/video/v5video/item/search.htm?', data,params);
  }

  /** * post 请求例子 */
  static fetchPostSample(data) {
    data._tb_token_ = _tb_token_;
    return fetchPost('/video/v5video/item/add.htm', data,params);
  }
  /** * jsonp 请求例子 */
  static fetchJsonpSample(tags) {
    return fetchJsonp('/common/jsonp/getTagName.htm?', {tags: tags},params);
  }
}

const data = {key1:value1,key2:value2}
IO.fetchPostSample({data})
.then(response=>{})
.catch(err=>{})

其他需要注意的组件

Tabs

注意如果两个tab有数据耦合,需要在切换时有针对性的刷新,比如将标签页面和标签维度放一个tabs中,如果在标签维度中添加了数据,那么切换回标签页面时,标签维度的select选项必须同步,最直接的做法就是刷新页面。
Button htmlType='submit’最常用
InputNumber 利用formatter和parser可以限制InputNumber的输入格式,比如限制用户只能输入一位小数
Spin 模拟loading状态

其它

使用diamond进行前后端分离和环境区分
目前采用diamond进行页面级别的前后端分离,工作流为:

  1. 后端提供接口和预埋数据,后台发布与前端发布隔离,这里的预埋数据前端可以通过window.paramName的全局方式获取;
  2. 后端提供日常,预发和线上三个环境的diamond地址,diamond中加入
    用于前端react渲染;
  3. 前端git发版本,手动在三个diamond环境内添加css和js资源,更新版本号;
  4. 由于不同的子后台域名不同,使用其他子后台接口时需要利用diamond区分接口环境,例如在优酷选品后台中要使用cms后台节目搜索接口,
    在日常的diamond中设置
    window.PROGRAM_SEARCH_URL = ‘//haibao.alibaba.net/youku/api/filter/show.json’;
    在预发和线上则改成
    window.PROGRAM_SEARCH_URL = ‘//haibao.alibaba.com/youku/api/filter/show.json’;前端通过window.PROGRAM_SEARCH_URL获得真实的接口地址。
  5. 存在的问题:diamond方案虽然可以使版本升级通过推送提高效率,但会产生前后端无法同时生效、回滚异常等副作用,需要进一步思考。