Ant Design Pro学习之使用upload组件并用form表单提交


阿里粑粑开源的管理框架Ant Design Pro使用记录之表单提交。
简书地址

效果

upload view

http network

api-debug

实现思路和代码

利用upload提供的beforeUpload属性,先将文件放到state里,随后和form表单一起提交。
先上干货,再解释一些走过的弯弯绕

接口代码
接受实体类


import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import javax.persistence.Transient;
import java.util.Date;

/**
 * @author 红创-马海强
 * @date 2019-02-20 14:06
 * @description 战略报告
 */
@Data
public class StrategyReportVo {

    private String id;

    private String title;
    private Date showTime;
    private String periods;
    private String fileUrl;
    private int deleteFlag = 0;
    private Date createTime;
    @Transient
    private int readCount;
    private MultipartFile[] files;
    private MultipartFile file;
    
}

API接口

 @PostMapping("/reports")
    public RtnResult update(StrategyReportVo vo) {
        StrategyReport report = new StrategyReport();
        BeanUtils.copyProperties(vo, report);
        return RtnResult.success(strategyReportAdminService.update(report));
    }

注意:接口使用form形式提交,因此在vo前面不能使用@RequestBody注解

前端为了方便先将fetch请求写在form页面里,规范的话应该写在model里。

import React, {PureComponent} from 'react';
import {Modal, Form, Input, Spin, DatePicker, Button, Icon, Upload} from 'antd';
import _ from 'lodash';
import FileUpload from '../Common/FileUpload';
import {uploadUrl} from '../../services/api-base';
import moment from "moment";
import {prefix} from '../../services/api';

const {Item: FormItem} = Form;

@Form.create()
export default class StrategyReportForm extends PureComponent {

    state = {
        fileData: [],
    }

    /** 文件上传属性 **/
    uploadProps = {
        accept: '.pdf',
        action: uploadUrl,
        name: 'files',
        onUpload: (fileList) => {
            this.props.onChangeFile(fileList);
        },
        onSuccess: (response) => {
            const {name, url} = response[0];
            const file = {
                uid: -1,
                name: name,
                status: 'done',
                url: url
            };
            this.props.form.setFieldsValue({fileUrl: url});
            this.props.onChangeFile([file]);
        },
        onRemove: () => {
            this.props.onChangeFile([]);
        }
    }


//这个是监听文件变化的
fileChange=(params)=>{
    const {file,fileList}=params;
    if(file.status==='uploading'){
        setTimeout(()=>{
            this.setState({
                percent:fileList.percent    
            })
        },1000)       
    }
}
// 拦截文件上传
beforeUploadHandle=(file)=>{
    this.setState(({fileData})=>({
        fileData:[...fileData,file],
    }))
    return false;
}
// 文件列表的删除
fileRemove=(file)=>{
    this.setState(({fileData})=>{
        const index = fileData.indexOf(file);
        return {
            fileData: fileData.filter((_, i) => i !== index)
        }
    })
}

    render() {
        const {modalVisible, formLoading, confirmLoading, data, onSave, onCancel, form, fileList} = this.props;
        const {getFieldDecorator} = this.props.form;
        const title = data.id ? '编辑报告' : '添加报告';
        const formItemLayout = {
            labelCol: {span: 5},
            wrapperCol: {span: 15},
        };
        const files = this.state.fileData;
        return (
            <Modal
                title=&#123;title&#125;
                visible=&#123;modalVisible&#125;
                confirmLoading=&#123;confirmLoading&#125;
                onOk=&#123;() => &#123;
                    form.validateFields((err, values) => &#123;
                        if (!err) &#123;
                          let formData = new FormData();
                          formData.append("file", files[0]);
                          for(let i = 0 ;i<files.length;i++)&#123;
                            //dataParament.files.fileList[i].originFileObj 这个对象是我观察 antd的Upload组件发现的里面的originFileObj 对象就是file对象
                            formData.append('files',files[i])
                           &#125;
                           //file以外的对象拼接
                           for(let item in values.length) &#123;
                             if(item !== 'files' && values[item]) &#123;
                                formData.append(item, values[item]);
                             &#125;
                           &#125;
                          fetch(`$&#123;prefix&#125;/questionnaire/admin/strategy/reports`, &#123;
                            method: 'POST',
                            body: formData,
                            headers: &#123;
                                'Authorization': `Bearer $&#123;sessionStorage.accessToken&#125;`,
                            &#125;,
                          &#125;).then((response => &#123;
                              if (response.code === 0) &#123;
                                  console.log("=====================", 'OK');
                              &#125; else &#123;
                                  console.log("=====================", 'error');
                              &#125;
                          &#125;));
                          onSave(data);
                        &#125;
                    &#125;);
                &#125;&#125;
                onCancel=&#123;onCancel&#125;>
                <Form id="postForm">
                    <Spin spinning=&#123;formLoading&#125; tip="加载中...">
                        &#123;
                            getFieldDecorator('id', &#123;initialValue: _.defaultTo(data.id, null)&#125;)
                        &#125;
                        <FormItem label="报告标题" &#123;...formItemLayout&#125;>
                            &#123;
                                getFieldDecorator('title', &#123;
                                    rules: [
                                        &#123;
                                            type: 'string',
                                            required: true,
                                            message: '标题不能为空!',
                                        &#125;,
                                    ],
                                    initialValue: _.defaultTo(data.title, ''),
                                &#125;)(<Input/>)
                            &#125;
                        </FormItem>
                        <FormItem label="显示时间" &#123;...formItemLayout&#125;>
                            &#123;
                                getFieldDecorator('showTime', &#123;
                                    rules: [
                                        &#123;
                                            required: true,
                                            message: '显示时间不能为空',
                                        &#125;,
                                    ],
                                    initialValue: data.showTime ? moment(moment(data.showTime).format('YYYY-MM-DD HH:mm')) : moment(),
                                &#125;)( <DatePicker showTime style=&#123;&#123;width: 280&#125;&#125; format="YYYY-MM-DD HH:mm"/>)
                            &#125;
                        </FormItem>
                        <FormItem label="指定期数" &#123;...formItemLayout&#125;>
                            &#123;
                                getFieldDecorator('periods', &#123;
                                    rules: [
                                        &#123;
                                            type: 'string',
                                            required: false,
                                            message: '期数',
                                        &#125;,
                                    ],
                                    initialValue: _.defaultTo(data.periods, ''),
                                &#125;)(<Input/>)
                            &#125;
                        </FormItem>
                        &#123;/* <FormItem label="上传附件" &#123;...formItemLayout&#125;>
                            &#123;
                                getFieldDecorator('fileUrl', &#123;
                                    rules: [
                                        &#123;
                                            type: 'string',
                                            required: true,
                                            message: '请上传PDF文档',
                                        &#125;,
                                    ],
                                    initialValue: _.defaultTo(data.fileUrl, '')
                                &#125;)(<FileUpload
                                    uploadProps=&#123;this.uploadProps&#125;
                                    fileList=&#123;fileList&#125;
                                    data=&#123;&#123;'objectKey': 'strategy/report'&#125;&#125;/>)
                            &#125;
                        </FormItem> */&#125;
                        <FormItem labelCol=&#123;&#123;span:5&#125;&#125; wrapperCol=&#123;&#123;span:15&#125;&#125; label='文件上传'>
                            &#123;getFieldDecorator('files')(
                                <Upload action='路径' 
                                    multiple uploadList 
                                    beforeUpload=&#123;this.beforeUploadHandle&#125; 
                                    onChange=&#123;this.fileChange&#125; 
                                    onRemove=&#123;this.fileRemove&#125; 
                                    fileList=&#123;this.state.fileData&#125;>
                                    <Button><Icon type='upload' />上传文件</Button>
                                </Upload>
                            )&#125;
                        </FormItem>
                    </Spin>
                </Form>
            </Modal>
        );
    &#125;

    componentWillReceiveProps(nextProps) &#123;
        if (!this.props.modalVisible && nextProps.modalVisible) &#123;
            this.props.form.resetFields();
        &#125;
    &#125;
&#125;

注意点

  • 1、Upload组件默认是选择文件后直接调用action上传文件,返回url。通常文件都会在form表单里跟别的参数一起,这时候form里其实没有文件,而是文件的url地址。
    就像下面这样。
    StrategyReportForm是这个弹出层,而它的上层页面是StrategyReportList,在list中的form是这样的
            <StrategyReportForm
                    modalVisible=&#123;strategyReportForm.modalVisible&#125;
                    confirmLoading=&#123;strategyReportForm.confirmLoading&#125;
                    options=&#123;strategyReportForm.options&#125;
                    data=&#123;strategyReportForm.data&#125;
                    fileList=&#123;strategyReportForm.fileList&#125;
                    formLoading=&#123;strategyReportForm.formLoading&#125;
                    onChangeFile=&#123;(fileList)=>&#123;
                      dispatch(&#123;type: 'strategyReportForm/fileList', payload: fileList&#125;);
                    &#125;&#125;
                    onSave=&#123;(data)=>&#123;
                      dispatch(&#123;type: 'strategyReportForm/update', payload: &#123;data, callback:(result)=>&#123;
                          dispatch(&#123;type: 'strategyReportList/list', payload:&#123;&#125;&#125;);
                      &#125;&#125;&#125;);
                  &#125;&#125;
                  onCancel=&#123;()=>&#123;
                      dispatch(&#123;type: 'strategyReportForm/close'&#125;);
             &#125;&#125;/>
    
    这段代码里的onSave回调方法的意思就是上传文件,关闭弹框,刷新列表。
    modle里的update方法与其他的没有两样。
      effects: &#123;
          * update(&#123;payload:&#123;data, callback&#125;&#125;, &#123;call, put, select&#125;)&#123;
              yield put(&#123;type: 'confirmLoading', payload: true&#125;);
              const response = yield call(api.update, data);
              if (response.code === 0) &#123;
                  message.success("操作成功");
                  yield put(&#123;type: 'close'&#125;);
                  if(callback) callback(response.data)
              &#125; else &#123;
                  message.error(response.message);
              &#125;
          &#125;,
      &#125;
    
    api.upload这个方法在antd pro里是隔离定义再service目录下的,内容很简单:
    export async function update(params) &#123;
    fetch(`$&#123;prefix&#125;/questionnaire/admin/strategy/reports`, &#123;
      method: 'POST',
      body: params,
      headers: &#123;
          'Authorization': `Bearer $&#123;sessionStorage.accessToken&#125;`,
      &#125;
    &#125;)
    &#125;
    
  • 需要注意的是这里得直接使用fetch方法,不能使用框架封装的request发起请求,因为request里封装的content-type类型是application/json*

在and design pro2.x的版本里,request方法已经兼容了这个处理
2.x request

在antd1.x的版本里,也可以使用reqeust里封装好的postFormWithProgress方法。比如这个用法:

<FormItem label="安装包地址" labelCol=&#123;&#123; span: 3 &#125;&#125; wrapperCol=&#123;&#123; span: 9 &#125;&#125;>
          &#123;
            getFieldDecorator('downloadAddr', &#123;
              rules: [
                &#123;
                  required: true,
                  message: '安装包地址不能为空',
                &#125;,
              ],
            &#125;)(
              <Input disabled />
            )
          &#125;
        </FormItem>
        <FormItem wrapperCol=&#123;&#123; offset:3, span: 9 &#125;&#125;>
          <Upload beforeUpload=&#123;this.uploadFile&#125;>
            <Button>
              <Icon type="upload" />上传文件
            </Button>
          </Upload>
          <Progress size="small" style=&#123;&#123; display: 'inline' &#125;&#125; percent=&#123;~~(this.state.uploadPercent*100)&#125; />
        </FormItem>

js

  uploadFile = (file) => &#123;
    this.setState(&#123; uploadPercent: 0 &#125;);
    uploadAppBinary(file, percent => this.setState(&#123; uploadPercent: percent &#125;)).then(
      (resp) => &#123;
        const &#123;
          code,
          message: msg,
          data,
        &#125; = resp;
        if (code === 0) &#123;
          const &#123; downloadAddr &#125; = data;
          this.props.form.setFieldsValue(&#123;
            downloadAddr,
          &#125;);
        &#125; else &#123;
          message.error(`上传文件失败!--$&#123;msg&#125;`);
        &#125;
      &#125;,
    ).catch(e => message.error(e.message));
    return false;
  &#125;

service

export async function uploadAppBinary(file, callback) &#123;
  return postFormWithProgress(`$&#123;prefix&#125;/questionnaire/admin/app/release/uploadPackage`, &#123;
    file,
  &#125;, callback);
&#125;
  • 2、但是这次不一样,我们文件先不上传,而是与form表单的其他内容一起提交到API里。解决问题是学到的东西不少,简单记录下。

    2.1、form里应不应该设置Content-Type属性,应该设置成什么?request里会有哪些不一样?
    直接参考post使用form-data和x-www-form-urlencoded的本质区别即可,但是结论是不需要自己设定,程序会自己根据类型设定。

2.2、调用接口时只要没有文件就没问题,但是有文件了就会400。
原因:多个文件的append不能直接把数组append进去,比如上面如果不用循环获取fileData里的数据,而是直接formData.append(this.state.fileData);这样的数据发送的接口,就会400,原因就是类型不对。
如果是单个文件,可以直接使用formData.append(files[0]);这样实现。

2.3、多个文件和单个文件的处理。
不论是单个文件或是多个文件,都可以使用循环的形式将文件append到formdata中。

  • 3、其他实现方式
    基于2.x以后的版本实现更简单一些。
    把json传到service的api以后,new出formData,append上参数即可。
    export async function batchImport(params)&#123;
      const formData = new FormData();
      for (const key in params) &#123;
          formData.append(key, params[key]);
      &#125;
      return request('/customer/batchImport', &#123; 
          method: 'POST',
          body: formData 
      &#125;);
    &#125;
    
    不过就是在form里要利用valuePropNamegetValueFromEvent属性把属性值以json的结构传递到modles里。
    <Modal
              destroyOnClose
              title="导入量体人"
              visible=&#123;batchImportShow&#125;
              onOk=&#123;this.handleOk&#125;
              onCancel=&#123;() => handleImportVisible(false)&#125;>
              <FormItem labelCol=&#123;&#123; span: 5 &#125;&#125; wrapperCol=&#123;&#123; span: 15 &#125;&#125; label="测量计划">
                  &#123;form.getFieldDecorator('planId',&#123;
                      rules: [&#123; required: true, message: '请选择测量计划', &#125;],
                  &#125;)(<Select style=&#123; &#123; width: 200 &#125;&#125; id='planSelect'>
                      <Select.Option key=&#123;-99&#125; value=''>全部</Select.Option>
                  &#123; planList.map((item) => <Select.Option key=&#123;item.planId&#125; value=&#123;item.planId&#125;>&#123;item.planName&#125;</Select.Option>) &#125;
                  </Select>)&#125;
              </FormItem>
              <FormItem labelCol=&#123;&#123; span: 5 &#125;&#125; wrapperCol=&#123;&#123; span: 15 &#125;&#125; label="数据文件">
                  &#123;form.getFieldDecorator('customerFile', &#123;
                      rules: [&#123; required: true, message: '请上传数据文件', &#125;],
                      valuePropName: 'files',
                      getValueFromEvent: e => e.target.files,
                  &#125;)(<Input type='file' name='customerFile' style=&#123;&#123;height:35&#125;&#125;/>)&#125;
              </FormItem>
            </Modal>
    

友情参考

将选中文件保存到页面的state中
将文件append到新的formdata中使用post方式提交


评论
 上一篇
转--BizCharts使用采坑教程 转--BizCharts使用采坑教程
最近项目的管理后台都在用阿里粑粑开源的管理框架Ant Design Pro,说真话,还是比较好用的。该框架内部也封装了一些图标插件,但是在最近的一个项目中发现,这些图标可定制性还是差了点,不能满足客户需求。 简书地址
下一篇 
Ant Design Pro 学习之跳页传参 Ant Design Pro 学习之跳页传参
阿里粑粑开源的管理框架Ant Design Pro使用记录之跳页传参。 简书地址
  目录