antd pro table中的文件上传
概述
项目中经常会遇到在表格中展示图片的需求(比如展示用户信息时, 有一列是用户的头像).
antd pro table 的功能很强大, 对于常规的信息展示只需参照示例配置 column 就可以了. 但是对于文件(比如图片) 在表格中的展示, 介绍并不多.
下面通过示例来演示 antd pro table 中图片的上传和展示.
示例代码
前端主要包含如下 2 部分:
- 列表页面: 通过 antd pro table 显示数据信息
- 表单页面: 新建/修改数据的页面, 上传图片的功能就在其中
一个模块主要包含如下几个文件:
- teacher.jsx: 显示数据列表信息
- teacher-form.jsx: 用于添加/修改数据
- model.js: list.jsx 和 form.jsx 之间共享数据
- service.js: 访问后端的 API
下面的例子是实际项目中的一个简单的模块, 完成教师信息的 CURD, 教师的头像是图片文件
列表页面
1 import React, { useState, useRef } from 'react';
2 import { connect } from 'umi';
3 import { PageHeaderWrapper } from '@ant-design/pro-layout';
4 import { Button, Card, Modal, Space, Popconfirm, Form, message } from 'antd';
5 import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
6 import ProTable from '@ant-design/pro-table';
7 import { queryAllTeacher, addTeacher, updateTeacher, deleteTeacher } from './service';
8 import { getDictDataByCatagory, getDownloadUrl } from '@/utils/common';
9 import TeacherForm from './teacher-form';
10
11 const Teacher = (props) => {
12 const { dicts, form, avatarFid } = props;
13 const [createModalVisible, handleModalVisible] = useState(false);
14
15 // preview state
16 const [previewVisible, handlePreviewVisible] = useState(false);
17 const [previewImageUrl, handlePreviewImageUrl] = useState('');
18
19 const [record, handleRecord] = useState(null);
20 const tableRef = useRef();
21
22 const previewAvatar = (record) => {
23 handlePreviewVisible(true);
24 if (record.avatar) handlePreviewImageUrl(getDownloadUrl(record.avatar));
25 else handlePreviewImageUrl('/nopic.jpg');
26 };
27
28 const teacherColumns = [
29 {
30 title: '头像图片',
31 dataIndex: 'avatar',
32 hideInSearch: true,
33 render: (_, record) => (
34 <a onClick={() => previewAvatar(record)}>
35 {record.avatar ? (
36 <img src={getDownloadUrl(record.avatar)} width={50} height={60} />
37 ) : (
38 <img src={'/nopic.jpg'} width={50} height={60} />
39 )}
40 </a>
41 ),
42 },
43 {
44 title: '姓名',
45 dataIndex: 'login_name',
46 },
47 {
48 title: '性别',
49 dataIndex: 'sex',
50 hideInSearch: true,
51 },
52 {
53 title: '手机号',
54 dataIndex: 'mobile',
55 },
56 {
57 title: '身份证号码',
58 dataIndex: 'identity_card',
59 hideInSearch: true,
60 },
61 {
62 title: '个人简介',
63 dataIndex: 'comment',
64 ellipsis: true,
65 width: 300,
66 hideInSearch: true,
67 },
68 {
69 title: '来源类型',
70 dataIndex: 'teacher_source',
71 hideInSearch: true,
72 valueEnum: getDictDataByCatagory(dicts, 'teacher_source'),
73 },
74 {
75 title: '操作',
76 dataIndex: 'option',
77 valueType: 'option',
78 render: (_, record) => (
79 <Space>
80 <Button
81 type="primary"
82 size="small"
83 onClick={() => {
84 handleRecord(record);
85 // 设置avatar数据
86 let avatarUrl = '/nopic.jpg';
87
88 if (record.avatar) avatarUrl = getDownloadUrl(record.avatar);
89
90 record.avatarFile = [
91 {
92 uid: '1',
93 name: 'avatar',
94 status: 'done',
95 url: avatarUrl,
96 },
97 ];
98 handleModalVisible(true);
99 }}
100 >
101 修改
102 </Button>
103 <Popconfirm
104 placement="topRight"
105 title="是否删除?"
106 okText="Yes"
107 cancelText="No"
108 onConfirm={async () => {
109 const response = await deleteTeacher(record.id);
110 if (response.code === 10000) message.info('教师: [' + record.login_name + '] 已删除');
111 else
112 message.warn('教师: [' + record.login_name + '] 有关联的课程和班级信息, 无法删除');
113 tableRef.current.reload();
114 }}
115 >
116 <Button danger size="small">
117 删除
118 </Button>
119 </Popconfirm>
120 </Space>
121 ),
122 },
123 ];
124
125 const okHandle = async () => {
126 const fieldsValue = await form.validateFields();
127 // handleAdd(fieldsValue);
128 console.log(fieldsValue);
129 fieldsValue.avatar = avatarFid;
130 const response = record
131 ? await updateTeacher(record.id, fieldsValue)
132 : await addTeacher(fieldsValue);
133
134 if (response.code !== 10000) {
135 if (
136 response.message.indexOf('Uniqueness violation') >= 0 &&
137 response.message.indexOf('teacher_mobile_key') >= 0
138 )
139 message.error('教师创建失败, 当前手机号已经存在');
140 }
141
142 if (response.code === 10000) {
143 handleModalVisible(false);
144 tableRef.current.reload();
145 }
146 };
147
148 return (
149 <PageHeaderWrapper title={false}>
150 <Card>
151 <ProTable
152 headerTitle="教师列表"
153 actionRef={tableRef}
154 rowKey="id"
155 toolBarRender={(action, { selectedRows }) => [
156 <Button
157 icon={<PlusOutlined />}
158 type="primary"
159 onClick={() => {
160 handleRecord(null);
161 handleModalVisible(true);
162 }}
163 >
164 新建
165 </Button>,
166 ]}
167 request={async (params) => {
168 const response = await queryAllTeacher(params);
169 return {
170 data: response.data.teacher,
171 total: response.data.teacher_aggregate.aggregate.count,
172 };
173 }}
174 columns={teacherColumns}
175 />
176 <Modal
177 destroyOnClose
178 forceRender
179 title="教师信息"
180 visible={createModalVisible}
181 onOk={okHandle}
182 onCancel={() => handleModalVisible(false)}
183 >
184 <TeacherForm record={record} />
185 </Modal>
186 <Modal
187 visible={previewVisible}
188 title={'用户头像'}
189 footer={null}
190 onCancel={() => handlePreviewVisible(false)}
191 >
192 <img alt="preview" style={{ width: '100%' }} src={previewImageUrl} />
193 </Modal>
194 </Card>
195 </PageHeaderWrapper>
196 );
197 };
198
199 export default connect(({ dict, teacher }) => ({
200 dicts: dict.dicts,
201 form: teacher.form,
202 avatarFid: teacher.avatarFid,
203 }))(Teacher);
form 页面
1 import React, { useState, useEffect } from 'react';
2 import _ from 'lodash';
3 import { connect } from 'umi';
4 import { formLayout } from '@/utils/common';
5 import { Form, Select, Input, Upload, Modal } from 'antd';
6 import { PlusOutlined, LoadingOutlined } from '@ant-design/icons';
7 import { upload } from '@/services/file';
8
9 const FormItem = Form.Item;
10 const { Option } = Select;
11 const { TextArea } = Input;
12
13 const TeacherForm = (props) => {
14 const { dispatch, dicts, record } = props;
15 const sexes = ['男', '女'];
16 const [fileList, handleFileList] = useState([]);
17 const [loading, handleLoading] = useState(false);
18 const [previewVisible, handlePreviewVisible] = useState(false);
19 const [previewTitle, handlePreviewTitle] = useState('');
20 const [previewImageUrl, handlePreviewImageUrl] = useState('');
21
22 const [form] = Form.useForm();
23 useEffect(() => {
24 if (form) {
25 form.resetFields();
26 dispatch({ type: 'teacher/setForm', payload: form });
27 }
28
29 // 初始化avatar
30 if (record && record.avatarFile) handleFileList(record.avatarFile);
31
32 if (record) dispatch({ type: 'teacher/setAvatarFid', payload: record.avatar });
33 else dispatch({ type: 'teacher/setAvatarFid', payload: '' });
34 }, []);
35
36 const handleChange = async ({ file, fileList }) => {
37 handleFileList(fileList);
38 if (file.status === 'uploading') handleLoading(true);
39 if (file.status === 'done') handleLoading(false);
40 };
41
42 const uploadButton = (
43 <div disabled>
44 {loading ? <LoadingOutlined /> : <PlusOutlined />}
45 <div className="ant-upload-text">上传照片</div>
46 </div>
47 );
48
49 const uploadAvatar = async ({ onSuccess, onError, file }) => {
50 const response = await upload('avatar', file);
51 try {
52 const {
53 code,
54 data: { fid },
55 } = response;
56
57 onSuccess(response, file);
58
59 dispatch({ type: 'teacher/setAvatarFid', payload: fid });
60 } catch (e) {
61 onError(e);
62 }
63 };
64
65 const previewImage = async (file) => {
66 handlePreviewVisible(true);
67 handlePreviewTitle(file.name);
68 let src = file.url;
69 if (!src) {
70 src = await new Promise((resolve) => {
71 const reader = new FileReader();
72 reader.readAsDataURL(file.originFileObj);
73 reader.onload = () => resolve(reader.result);
74 });
75 }
76 handlePreviewImageUrl(src);
77 };
78
79 const removeImage = () => {
80 handleFileList([]);
81 dispatch({ type: 'teacher/setAvatarFid', payload: '' });
82 };
83
84 const normFile = (e) => {
85 if (Array.isArray(e)) {
86 return e;
87 }
88 return e && e.fileList;
89 };
90
91 const uploadProps = {
92 name: 'avatar',
93 listType: 'picture-card',
94 className: 'avatar-uploader',
95 customRequest: uploadAvatar,
96 onPreview: previewImage,
97 onRemove: removeImage,
98 fileList: fileList,
99 };
100
101 return (
102 <div>
103 <Form form={form} {...formLayout} initialValues={record ? { ...record } : ''}>
104 <FormItem
105 label="来源类型"
106 name="teacher_source"
107 rules={[
108 {
109 required: true,
110 },
111 ]}
112 >
113 <Select
114 style={{
115 width: '100%',
116 }}
117 >
118 {_.filter(dicts, (d) => d.catagory === 'teacher_source').map((r) => (
119 <Option key={r.id} value={r.key}>
120 {r.val}
121 </Option>
122 ))}
123 </Select>
124 </FormItem>
125 <FormItem
126 label="姓名"
127 name="login_name"
128 rules={[
129 {
130 required: true,
131 },
132 ]}
133 >
134 <Input placeholder="姓名" />
135 </FormItem>
136 <FormItem
137 label="性别"
138 name="sex"
139 rules={[
140 {
141 required: true,
142 },
143 ]}
144 >
145 <Select
146 style={{
147 width: '100%',
148 }}
149 >
150 {sexes.map((r) => (
151 <Option key={r} value={r}>
152 {r}
153 </Option>
154 ))}
155 </Select>
156 </FormItem>
157 <FormItem
158 label="手机号"
159 name="mobile"
160 rules={[
161 {
162 pattern: new RegExp(/^1[3-9]\d{9}$/, 'g'),
163 message: '手机号格式不正确',
164 },
165 ]}
166 >
167 <Input placeholder="手机号" />
168 </FormItem>
169 <FormItem label="身份证号码" name="identity_card">
170 <Input placeholder="身份证号码" />
171 </FormItem>
172 <FormItem label="个人简介" name="comment">
173 <TextArea rows={4} placeholder="个人简介" />
174 </FormItem>
175 <FormItem
176 label="用户头像"
177 name="avatarFile"
178 valuePropName="fileList"
179 getValueFromEvent={normFile}
180 >
181 <Upload {...uploadProps} onChange={handleChange}>
182 {fileList.length >= 1 ? null : uploadButton}
183 </Upload>
184 </FormItem>
185 </Form>
186 <Modal
187 visible={previewVisible}
188 title={previewTitle}
189 footer={null}
190 onCancel={() => handlePreviewVisible(false)}
191 >
192 <img alt="preview" style={{ width: '100%' }} src={previewImageUrl} />
193 </Modal>
194 </div>
195 );
196 };
197
198 export default connect(({ dict }) => ({
199 dicts: dict.dicts,
200 }))(TeacherForm);
model.js
1 import { message } from 'antd';
2
3 const Model = {
4 namespace: 'teacher',
5 state: {
6 form: null,
7 avatarFid: '',
8 },
9
10 effects: {},
11 reducers: {
12 setForm(state, { payload }) {
13 return {
14 ...state,
15 form: payload,
16 };
17 },
18 setAvatarFid(state, { payload }) {
19 return {
20 ...state,
21 avatarFid: payload,
22 };
23 },
24 },
25 };
26 export default Model;
service.js
1 import { graphql } from '@/services/graphql_client';
2 import md5 from 'md5';
3 import moment from 'moment';
4
5 const gqlQueryAll = `
6 query search_teacher($login_name: String, $mobile: String, $limit: Int!, $offset: Int!) {
7 teacher(order_by: {updated_at: desc}, limit: $limit, offset: $offset, where: {login_name: {_ilike: $login_name}, mobile: {_ilike: $mobile}}) {
8 id
9 avatar
10 comment
11 identity_card
12 login_name
13 mobile
14 sex
15 teacher_source
16 }
17 teacher_aggregate(where: {login_name: {_ilike: $login_name}, mobile: {_ilike: $mobile}}) {
18 aggregate {
19 count
20 }
21 }
22 }
23 `;
24
25 const qplAddTeacher = `
26 mutation add_teacher($avatar: uuid, $comment: String, $identity_card: String, $login_name: String!, $mobile: String, $sex: String!, $teacher_source: String!, $password: String!){
27 insert_teacher_one(object: {avatar: $avatar, comment: $comment, identity_card: $identity_card, login_name: $login_name, mobile: $mobile, sex: $sex, teacher_source: $teacher_source, password: $password}) {
28 id
29 }
30 }
31 `;
32
33 const qplUpdateTeacher = `
34 mutation update_teacher($id: uuid!, $avatar: uuid, $comment: String, $identity_card: String, $login_name: String, $mobile: String, $sex: String, $teacher_source: String) {
35 update_teacher_by_pk(_set: {avatar: $avatar, comment: $comment, identity_card: $identity_card, login_name: $login_name, mobile: $mobile, sex: $sex, teacher_source: $teacher_source}, pk_columns: {id: $id}) {
36 id
37 }
38 }
39 `;
40
41 const qplDeleteTeacher = `
42 mutation del_teacher($id: uuid!){
43 delete_teacher_by_pk(id: $id) {
44 id
45 }
46 }
47 `;
48
49 export async function queryAllTeacher(params) {
50 let qplVar = {
51 limit: params.pageSize,
52 offset: (params.current - 1) * params.pageSize,
53 };
54
55 if (params.login_name) qqlVar.login_name = '%' + params.login_name + '%';
56 if (params.mobile) qqlVar.mobile = '%' + params.mobile + '%';
57
58 return graphql(gqlQueryAll, qplVar);
59 }
60
61 export async function addTeacher(params) {
62 const { avatar, comment, identity_card, mobile, sex, login_name, teacher_source } = params;
63
64 let insertVar = { login_name, sex, mobile, teacher_source };
65 if (avatar !== '') insertVar.avatar = avatar;
66 if (identity_card) insertVar.identity_card = identity_card;
67 if (comment) insertVar.comment = comment;
68 if (mobile) {
69 insertVar.mobile = mobile;
70 insertVar.password = md5(mobile.slice(-6));
71 } else {
72 // default password
73 insertVar.password = md5('123456');
74 }
75
76 return graphql(qplAddTeacher, {
77 ...insertVar,
78 });
79 }
80
81 export async function updateTeacher(id, params) {
82 let { avatar, comment, identity_card, mobile, sex, login_name, teacher_source } = params;
83 if (avatar === '') avatar = null;
84 return graphql(qplUpdateTeacher, {
85 id,
86 avatar,
87 comment,
88 identity_card,
89 mobile,
90 sex,
91 login_name,
92 teacher_source,
93 });
94 }
95
96 export async function deleteTeacher(id) {
97 return graphql(qplDeleteTeacher, { id });
98 }
service.js 中的请求是 graphql api
总结
这个模块的 增和改 用的同一个页面, 因为是弹出的 modal, 所有实际的提交功能是在 teacher.jsx 中完成的
antd upload 组件的 外围 FormItem 需要加上如下属性(valuePropName 和 getValueFromEvent):
1 <FormItem
2 label="用户头像"
3 name="avatarFile"
4 valuePropName="fileList"
5 getValueFromEvent={normFile}
6 >
7 <Upload />
8 </FormItem>
antd upload 组件虽然有默认的上传事件, 但是如果自定义上传的事件, 可以更方便的和自己的后端 API 进行对接
1 const uploadAvatar = async ({ onSuccess, onError, file }) => {
2 const response = await upload('avatar', file);
3 try {
4 const {
5 code,
6 data: { fid },
7 } = response;
8
9 onSuccess(response, file);
10
11 dispatch({ type: 'teacher/setAvatarFid', payload: fid });
12 } catch (e) {
13 onError(e);
14 }
15 };
antd pro table中的文件上传的更多相关文章
- struts2中的文件上传,文件下载
文件上传: Servlet中的文件上传回顾 前台页面 1.提交方式post 2.表单类型 multipart/form-data 3.input type=file 表单输入项 后台 apache提交 ...
- IIS 7 中设置文件上传大小的方法
在IIS 6.0中设置文件上传大小的方法,就是配置如下节点: <system.web> <httpRuntime maxRequestLength="1918200&quo ...
- 在WebBrowser中通过模拟键盘鼠标操控网页中的文件上传控件(转)
引言 这两天沉迷了Google SketchUp,刚刚玩够,一时兴起,研究了一下WebBrowser. 我在<WebBrowser控件使用技巧分享>一文中曾谈到过“我现在可以通过WebBr ...
- PHP中,文件上传实例
PHP中,文件上传一般是通过move_uploaded_file()来实现的. bool move_uploaded_file ( string filename, string destinati ...
- MVC中的文件上传-小结
web开发中,文件的上传是非常基本功能之一. 在asp.net中,通常做法是利用webservice 来接收文件请求,这样做的好处就是全站有了一个统一的文件上传接口,并且根据网站的实际情况,可以将we ...
- ASP.NET中的文件上传大小限制的问题
一.文件大小限制的问题 首先我们来说一下如何解决ASP.NET中的文件上传大小限制的问题,我们知道在默认情况下ASP.NET的文件上传大小限制为2M,一般情况下,我们可以采用更改WEB.Config文 ...
- 转:在Struts 2中实现文件上传
(本文转自:http://www.blogjava.net/max/archive/2007/03/21/105124.html) 前一阵子有些朋友在电子邮件中问关于Struts 2实现文件上传的问题 ...
- ASP.NET Core 中的文件上传
ASP.NET Core上传文件 ASP.NET Core使用IFormFile来读取上传的文件内容,然后将数据写入到磁盘或其它存储空间. 添加FileUpload模型,用来接收上传的文件内容. pu ...
- javaWeb中的文件上传下载
在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现. 对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用 ...
随机推荐
- Python 利用三个简易模块熟悉前后端交互流程
准备工作 在学习Django之前,先动手撸一个简单的WEB框架来熟悉一下前后端交互的整体流程 本次用到的模块: 1.wsgiref,这是一个Python自带的模块,用于构建路由与视图 2.pymysq ...
- 不要再纠结css/js/html有没有必要放在WEB-INF下了
原因 首先,css/js/html没有必要放在WEB-INF下. 最终这些会被原封不动的展现在客户端,所以访问安全根本就不会成为问题. jsp放在web-inf下,原因主要有两个 1. 远古时代的模式 ...
- OpenvSwitch系列之六 vlan隔离
局域网游戏代表:红色警戒 Open vSwitch系列之一 Open vSwitch诞生 Open vSwitch系列之二 安装指定版本ovs Open vSwitch系列之三 ovs-vsctl命令 ...
- Hexo + Github Pages搭建个人网站主页
1.GitHub创建个人仓库 登录GitHub创建账号,同时拥有一个自己设定的用户名(username).点击New Repositories创建仓库.仓库名必须为username.github.io ...
- get、post请求方式在postman中使用步骤
1.get请求方式:不需要借助任何工具,在浏览器里面就可以发送请求,直接在浏览器里面输入访问 url?参数名=参数值 url?parma=abc&name=abcd 2.post请 ...
- html+css入门基础案例之页面设计
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- canvas学习作业,模仿做一个祖玛的小游戏
这个游戏的原理我分为11个步骤,依次如下: 1.布局, 2.画曲线(曲线由两个半径不同的圆构成) 3.画曲线起点起始圆和曲线终点终止圆 4.起始的圆动起来, 5.起始的圆沿曲线走起来 6.起始的圆沿曲 ...
- 基于Prometheus网关的监控完整实现参考
prometheus 是一个非常好的监控组件,尤其是其与grafana配合之后,更是如虎添翼.而prometheus的监控有两种实现方式.1. server端主动拉取应用监控数据:2. 主动推送监控数 ...
- python获取某视频网站视频
还是老生常谈的操作 import requests import os from bs4 import BeautifulSoup from urllib.parse import urljoin h ...
- Java List 常用集合 ArrayList、LinkedList、Vector
Java 中的 List 是非常常用的数据类型.List 是有序的 Collection,Java List 一共有三个实现类,分别是:ArrayList.Vector.LinkedList 本文分析 ...