概述

项目中经常会遇到在表格中展示图片的需求(比如展示用户信息时, 有一列是用户的头像).

antd pro table 的功能很强大, 对于常规的信息展示只需参照示例配置 column 就可以了. 但是对于文件(比如图片) 在表格中的展示, 介绍并不多.

下面通过示例来演示 antd pro table 中图片的上传和展示.

示例代码

前端主要包含如下 2 部分:

  1. 列表页面: 通过 antd pro table 显示数据信息
  2. 表单页面: 新建/修改数据的页面, 上传图片的功能就在其中

一个模块主要包含如下几个文件:

  1. teacher.jsx: 显示数据列表信息
  2. teacher-form.jsx: 用于添加/修改数据
  3. model.js: list.jsx 和 form.jsx 之间共享数据
  4. 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

总结

  1. 这个模块的 增和改 用的同一个页面, 因为是弹出的 modal, 所有实际的提交功能是在 teacher.jsx 中完成的

  2. antd upload 组件的 外围 FormItem 需要加上如下属性(valuePropName 和 getValueFromEvent):

    1  <FormItem
    2 label="用户头像"
    3 name="avatarFile"
    4 valuePropName="fileList"
    5 getValueFromEvent={normFile}
    6 >
    7 <Upload />
    8 </FormItem>
  3. 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中的文件上传的更多相关文章

  1. struts2中的文件上传,文件下载

    文件上传: Servlet中的文件上传回顾 前台页面 1.提交方式post 2.表单类型 multipart/form-data 3.input type=file 表单输入项 后台 apache提交 ...

  2. IIS 7 中设置文件上传大小的方法

    在IIS 6.0中设置文件上传大小的方法,就是配置如下节点: <system.web> <httpRuntime maxRequestLength="1918200&quo ...

  3. 在WebBrowser中通过模拟键盘鼠标操控网页中的文件上传控件(转)

    引言 这两天沉迷了Google SketchUp,刚刚玩够,一时兴起,研究了一下WebBrowser. 我在<WebBrowser控件使用技巧分享>一文中曾谈到过“我现在可以通过WebBr ...

  4. PHP中,文件上传实例

    PHP中,文件上传一般是通过move_uploaded_file()来实现的.  bool move_uploaded_file ( string filename, string destinati ...

  5. MVC中的文件上传-小结

    web开发中,文件的上传是非常基本功能之一. 在asp.net中,通常做法是利用webservice 来接收文件请求,这样做的好处就是全站有了一个统一的文件上传接口,并且根据网站的实际情况,可以将we ...

  6. ASP.NET中的文件上传大小限制的问题

    一.文件大小限制的问题 首先我们来说一下如何解决ASP.NET中的文件上传大小限制的问题,我们知道在默认情况下ASP.NET的文件上传大小限制为2M,一般情况下,我们可以采用更改WEB.Config文 ...

  7. 转:在Struts 2中实现文件上传

    (本文转自:http://www.blogjava.net/max/archive/2007/03/21/105124.html) 前一阵子有些朋友在电子邮件中问关于Struts 2实现文件上传的问题 ...

  8. ASP.NET Core 中的文件上传

    ASP.NET Core上传文件 ASP.NET Core使用IFormFile来读取上传的文件内容,然后将数据写入到磁盘或其它存储空间. 添加FileUpload模型,用来接收上传的文件内容. pu ...

  9. javaWeb中的文件上传下载

    在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现. 对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用 ...

随机推荐

  1. ubuntu 18.04下修改python3指向

    起因 ubuntu18.04下默认带的是python3.6,但是因为需求需要升级为python3.7 步骤 安装 sudo apt install python3.7 修改环境变量 修改默认的pyth ...

  2. 操作BOM对象

    操作BOM对象 目录 操作BOM对象 1. 浏览器介绍 2. window 3. Navigator(不建议使用) 4. screan 5. location(重要) 6. document(内容:D ...

  3. RocketMQ的发送模式和消费模式

    前言 小伙伴们大家好啊,王子又来和大家一起闲谈MQ技术了. 通过之前文章的学习,我们已经对RocketMQ的基本架构有了初步的了解,那今天王子就和大家一起来点实际的,用代码和大家一起看看RocketM ...

  4. python实例基础(慢慢补充)

    1.有四个数字:1.2.3.4,能组成多少个互不相同且无重复数字的三位数?各是多少? 2.打印出所有的"水仙花数",所谓"水仙花数"是指一个三位数,其各位数字立 ...

  5. SpringMVC-12-SSM回顾与总结

    12.SSM回顾与总结

  6. Spring使用@Async实现异步

    使用场景 在实际项目中,一个接口如果需要处理很多数据,如果是同步执行,通过网络请求接口可能会出现请求超时.这时候就需要使用异步执行处理了. 使用经验 代码 异步服务类 @Service // Spri ...

  7. Linux实战(17):Linux配置用户登陆时发送邮件到指定邮箱

    参考其他文章,正好有这个需求,记一笔做个记录,以防丢失. 参考链接 #!/bin/bash yum install -y mailx cat >> /etc/mail.rc<< ...

  8. RXJAVA之变换操作

    RXJAVA提供了以下变换操作,对Observable的消息进行变换操作: 1.window 定期将来自Observable的数据分拆成一些Observable窗口,然后发射这些窗口,而不是每次发射一 ...

  9. 分布式服务(RPC)+分布式消息队列(MQ)面试题精选

    ​ 分布式系统(distributed system)是建立在网络之上的软件系统.正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性.因此,网络和分布式系统之间的区别更多的在于高层软件(特别是 ...

  10. linux下Crontab定时任务

    1.命令格式 crontab [-u user] file crontab [-u user] [-e | -l | -r ] 2.命令参数 -u user:用来设定某个用户的crontab服务: f ...