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

效果

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

    componentWillReceiveProps(nextProps) {
        if (!this.props.modalVisible && nextProps.modalVisible) {
            this.props.form.resetFields();
        }
    }
}

注意点

  • 1、Upload组件默认是选择文件后直接调用action上传文件,返回url。通常文件都会在form表单里跟别的参数一起,这时候form里其实没有文件,而是文件的url地址。
    就像下面这样。
    StrategyReportForm是这个弹出层,而它的上层页面是StrategyReportList,在list中的form是这样的
          <StrategyReportForm
                  modalVisible={strategyReportForm.modalVisible}
                  confirmLoading={strategyReportForm.confirmLoading}
                  options={strategyReportForm.options}
                  data={strategyReportForm.data}
                  fileList={strategyReportForm.fileList}
                  formLoading={strategyReportForm.formLoading}
                  onChangeFile={(fileList)=>{
                    dispatch({type: 'strategyReportForm/fileList', payload: fileList});
                  }}
                  onSave={(data)=>{
                    dispatch({type: 'strategyReportForm/update', payload: {data, callback:(result)=>{
                        dispatch({type: 'strategyReportList/list', payload:{}});
                    }}});
                }}
                onCancel={()=>{
                    dispatch({type: 'strategyReportForm/close'});
           }}/>

这段代码里的onSave回调方法的意思就是上传文件,关闭弹框,刷新列表。
modle里的update方法与其他的没有两样。

    effects: {
        * update({payload:{data, callback}}, {call, put, select}){
            yield put({type: 'confirmLoading', payload: true});
            const response = yield call(api.update, data);
            if (response.code === 0) {
                message.success("操作成功");
                yield put({type: 'close'});
                if(callback) callback(response.data)
            } else {
                message.error(response.message);
            }
        },
    }

api.upload这个方法在antd pro里是隔离定义再service目录下的,内容很简单:

export async function update(params) {
  fetch(`${prefix}/questionnaire/admin/strategy/reports`, {
    method: 'POST',
    body: params,
    headers: {
        'Authorization': `Bearer ${sessionStorage.accessToken}`,
    }
  })
}

需要注意的是这里得直接使用fetch方法,不能使用框架封装的request发起请求,因为request里封装的content-type类型是application/json

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


2.x request

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

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

js

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

service

export async function uploadAppBinary(file, callback) {
  return postFormWithProgress(`${prefix}/questionnaire/admin/app/release/uploadPackage`, {
    file,
  }, callback);
}
  • 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){
    const formData = new FormData();
    for (const key in params) {
        formData.append(key, params[key]);
    }
    return request('/customer/batchImport', { 
        method: 'POST',
        body: formData 
    });
}

不过就是在form里要利用valuePropNamegetValueFromEvent属性把属性值以json的结构传递到modles里。

<Modal
            destroyOnClose
            title="导入量体人"
            visible={batchImportShow}
            onOk={this.handleOk}
            onCancel={() => handleImportVisible(false)}>
            <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="测量计划">
                {form.getFieldDecorator('planId',{
                    rules: [{ required: true, message: '请选择测量计划', }],
                })(<Select style={ { width: 200 }} id='planSelect'>
                    <Select.Option key={-99} value=''>全部</Select.Option>
                { planList.map((item) => <Select.Option key={item.planId} value={item.planId}>{item.planName}</Select.Option>) }
                </Select>)}
            </FormItem>
            <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="数据文件">
                {form.getFieldDecorator('customerFile', {
                    rules: [{ required: true, message: '请上传数据文件', }],
                    valuePropName: 'files',
                    getValueFromEvent: e => e.target.files,
                })(<Input type='file' name='customerFile' style={{height:35}}/>)}
            </FormItem>
          </Modal>

友情参考

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • XMLHttpRequest Level 2添加了一个新的接口FormData.利用FormData对象,我们可以...
    冇得感情阅读 1,050评论 0 0
  • 前言 使用ajax请求数据,很多人都会,比如说: $.post(path,{data:data},function...
    WwKk_13c2阅读 3,490评论 0 8
  • # Ajax标签(空格分隔): 笔记整理---[TOC]### 从输入网址开始:- 在学习ajax之前,你应该先了...
    V8阅读 250评论 1 0
  • 一. Java基础部分.................................................
    wy_sure阅读 3,785评论 0 11
  • 被太阳 烤得炙热的石道 我 走在上面 被微风 吹得摇曳的新芽 我 望着它们 被花香 迷得陶醉的人儿 我 路过你们 ...
    秋月姑娘阅读 203评论 3 9