效果
实现思路和代码
利用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方法已经兼容了这个处理
在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里要利用valuePropName
和getValueFromEvent
属性把属性值以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>