antd pro table中的文件上传

  • 概述
  • 示例代码
    • 列表页面
    • form 页面
    • model.js
    • service.js
  • 总结

概述

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

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  };
    
(0)

相关推荐

  • Electron+React 快速搭建一个桌面应用

    一.项目技术栈:Electron+react+react-router+antd1.Electron:electron是一个使用js,html和css等的web技术创建原生桌面应用的框架,他基于chr ...

  • 基于业务场景下的图片/文件上传方案总结

    图片/文件上传组是企业项目开发中必不可少的环节之一, 但凡涉及到用户模块的都会有图片/文件上传需求, 在很多第三方组件库(ant desigin,element ui)中它也是基础组件之一. 接下来笔 ...

  • React+electron项目搭建 打包

    一.搭建react+electron项目 1.创建一个react项目 create-react-app my-app cd my-app npm start 看下页面是否打开,是否运行正确. 注意:如 ...

  • AntD框架的upload组件上传图片时使用customRequest方法自定义上传行为

    本次做后台管理系统,采用的是 AntD 框架.涉及到图片的上传,用的是AntD的 upload 组件. 我在上一篇文章<前端AntD框架的upload组件上传图片时遇到的一些坑>中讲到:A ...

  • react-app-rewired

    介绍 create-creact-app项目,如果需要手动修改配置,需先npm run eject弹出配置,这个过程是不可逆的 推荐使用第三方工具进行修改 这里介绍使用react-app-rewire ...

  • 强大的table组件-antd pro table

    概述 antd pro table antd pro table 的主要部分 表格显示的配置(绿色框内) 检索的配置(红色框内) 是否显示检索部分 检索的内容是如何生效的 工具栏的配置(黄色框内) 表 ...

  • 一文教你实现java中的文件上传下载

    大家好,我是小编LRyab,公众号LRyab博客网,LRyab博客网是分享一些个人成长.思考方向.个人感悟.职业规划,博客涵盖java技术 vue. html. css.前端技术spring全家桶. ...

  • SpringMVC中的文件上传

    这是用的是SpringMVC-3.1.1.commons-fileupload-1.2.2和io-2.0.1 web.xml文件 <?xml version="1.0" en ...

  • SpringBoot 多文件上传、携带参数

    参考文章: https://stackoverflow.com/questions/36005436/the-request-was-rejected-because-no-multipart-bou ...

  • 文件上传的单元测试怎么写?

    早上有个群友问了一个不错的问题:文件上传的单元测试怎么写?后面也针对后端开发要不要学一下单元测试的话题聊了聊,个人是非常建议后端开发能够学一下单元测试的.所以,今天特地拿出来写一篇说说,并不是因为这有 ...

  • 文件upload 文件上传深入

    我记录这篇文章是因为开发过程中,发现上传业务有时候感觉不同平台自己有时一脸懵逼不知道咋样去优化这块业务,不同的后台实现咋样做不同的处理,以下介绍后台实现主要node和java为主,比如:手机端app图 ...

  • Selenium2+python自动化75-非input文件上传(SendKeys)

    前言 不少小伙伴问非input标签如何上传文档,这个本身就是一坑,无奈很多小伙伴非要跳坑里去,那就介绍一个非主流的上传文件方法吧,用第三方库SendKeys. (本篇基于python2.7版本的,py ...

  • selenium+python自动化77-autoit文件上传

    前言 关于非input文件上传,点上传按钮后,这个弹出的windows的控件了,已经跳出三界之外了,不属于selenium的管辖范围(selenium不是万能的,只能操作web上元素).autoit工 ...

  • python测试开发django-rest-framework-95.文件上传接口开发

    前言 django-rest-framework 开发文件上传接口 新建模型 models.py 创建模型 from django.db import models # 作者-上海悠悠 QQ交流群:7 ...

  • postman使用教程16-测试文件上传接口(content-type: multipart/form-data )

    前言 使用 postman 测试文件上传接口,文件上传请求头部参数是content-type: multipart/form-data 类型 文件上传 新建一个request请求,选post请求方式, ...