zoukankan      html  css  js  c++  java
  • antd pro table中的文件上传

    概述

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

    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         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={{  '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                 '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                 '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={{  '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  };
      
  • 相关阅读:
    类别的三个作用
    require()
    commonJS
    ng-app&data-ng-app
    《css网站布局实录》(李超)——读书札记
    高性能JS(读书札记)
    两个同级div重叠的情况
    前端性能优化
    正则表达式
    ajax
  • 原文地址:https://www.cnblogs.com/wang_yb/p/13647236.html
Copyright © 2011-2022 走看看