项目开始

  • 前言: 目前新的 umi4.x 版本,ant-design-pro 最新有v6.0.0 版本,此项目比较适合新手学习 umi, 有 react 基础再学习。
  • 新建项目 新建文件夹umi3-antdpro5-ts, 下面执行命令 yarn create @umijs/umi-app,版本为:umi 3.5.35 版本
  • package.json 文件
{
  "private": true,
  "scripts": {
    "start": "umi dev",
    "build": "umi build",
    "postinstall": "umi generate tmp",
    "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
    "test": "umi-test",
    "test:coverage": "umi-test --coverage"
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,less,md,json}": [
      "prettier --write"
    ],
    "*.ts?(x)": [
      "prettier --parser=typescript --write"
    ]
  },
  "dependencies": {
    "@ant-design/pro-layout": "^6.5.0",
    "@ant-design/pro-table": "^2.2.7",
    "moment": "^2.29.4",
    "react": "17.x",
    "react-dom": "17.x",
    "umi": "^3.5.35"
  },
  "devDependencies": {
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@umijs/preset-react": "1.x",
    "@umijs/test": "^3.5.35",
    "lint-staged": "^10.0.7",
    "prettier": "^2.2.0",
    "typescript": "^4.1.2",
    "yorkie": "^2.0.0"
  }
}

新建页面

  • page 目录 新建文件夹 users
  • .umirc.ts 自定义路由先注释掉
  • 新建文件,文件目录
.
├── components
│   └── UserModal.tsx
├── index.tsx
├── model.ts
├── service.ts
└── type.d.ts

使用自带 AntD 组件

  • 整合内置 dva

RUNOOB 图标

import React, { FC } from 'react';
import { Button, Popconfirm, Space, Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { connect, Dispatch } from 'dva';
import { Loading } from '@@/plugin-dva/connect';
import UserModal from './components/UserModal';
import { SingleUserType, UserModelState } from './type.d';

interface UserPageProps {
  users: UserModelState;
  dispatch: Dispatch;
  useListLoading: boolean;
}

const UsersPageList: FC<UserPageProps> = ({
  users,
  dispatch,
  useListLoading,
}) => {
  const [modalVisible, setModalVisible] = React.useState(false);
  const [record, setRecord] = React.useState<SingleUserType | undefined>(
    undefined,
  );

  const columns: ColumnsType<SingleUserType> = [
    {
      title: 'ID',
      dataIndex: 'id',
      key: 'id',
    },
    {
      title: 'Name',
      dataIndex: 'name',
      key: 'name',
      render: (text) => <a>{text}</a>,
    },
    {
      title: 'CreateTime',
      dataIndex: 'create_time',
      key: 'create_time',
    },

    {
      title: 'Action',
      key: 'action',
      render: (_, record: SingleUserType) => (
        <Space size="middle">
          <a onClick={() => handleEdit(record)}>Edit</a>
          <a>
            <Popconfirm
              title="是否要删除这条记录?"
              onConfirm={() => comform(record)}
            >
              Delete
            </Popconfirm>
          </a>
        </Space>
      ),
    },
  ];
  // 编辑
  const handleEdit = (record: SingleUserType) => {
    setModalVisible(true);
    setRecord(record);
  };
  // 关闭弹窗
  const handleColse = () => {
    setModalVisible(false);
    setRecord(undefined);
  };
  // 编辑提交
  const onFinish = (values: SingleUserType) => {
    let id = 0;
    if (record) {
      id = record.id;
    }
    if (id) {
      dispatch({
        type: 'users/edit',
        payload: {
          id,
          values,
        },
      });
    } else {
      dispatch({
        type: 'users/add',
        payload: {
          values,
        },
      });
    }

    setModalVisible(false);
  };
  const handleAdd = () => {
    setModalVisible(true);
    setRecord(undefined);
  };
  // 删除
  const comform = (record: SingleUserType) => {
    dispatch({
      type: 'users/del',
      payload: {
        id: record.id,
      },
    });
  };
  // 提交失败
  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };
  return (
    <div className="list-table">
      <Button type="primary" onClick={handleAdd}>
        Add
      </Button>
      <Table
        loading={useListLoading}
        columns={columns}
        dataSource={users.data}
        rowKey="id"
      />
      <UserModal
        visible={modalVisible}
        handleColse={handleColse}
        record={record}
        onFinish={(values) => {
          onFinish(values);
        }}
        onFinishFailed={(errorInfo) => {
          onFinishFailed(errorInfo);
        }}
      />
    </div>
  );
};

const mapStateToProps = ({
  users,
  loading,
}: {
  users: [];
  loading: Loading;
}) => {
  console.log('users', users);
  return {
    users,
    useListLoading: loading.models.users,
  };
};

export default connect(mapStateToProps)(UsersPageList);

import { Effect, Subscription } from 'dva';
import { Reducer } from 'redux';
import { getRemoteList, editRecord, deleteRecord, addRecord } from './service';
import { UserModelState } from './type.d';

export interface UserModelType {
  namespace: 'users';
  state: UserModelState;
  effects: {
    query: Effect;
    getRemote: Effect;
    edit: Effect;
    del: Effect;
    add: Effect;
  };
  reducers: {
    save: Reducer;
    getList: Reducer<UserModelState>;
    // 启用 immer 之后
    // save: ImmerReducer<UserModelState>;
  };
  subscriptions: { setup: Subscription };
}

const UserModel: UserModelType = {
  namespace: 'users',

  state: {
    data: [],
    meta: {
      total: 0,
      per_page: 10,
      page: 1,
    },
  },

  effects: {
    *query({ payload }, { call, put }) {
      console.log('query');
    },
    // 获取远程数据
    *getRemote({ payload }, { call, put }): any {
      const data = yield call(getRemoteList, payload);
      yield put({
        type: 'getList',
        payload: data,
      });
    },
    // 编辑用户
    *edit({ payload }, { call, put }): any {
      const data = yield call(editRecord, payload);
      // 刷新列表
      yield put({
        type: 'getRemote',
      });
    },
    //添加用户
    *add({ payload }, { call, put }): any {
      const data = yield call(addRecord, payload);
      // 刷新列表
      yield put({
        type: 'getRemote',
      });
    },
    // 删除用户
    *del({ payload }, { call, put }): any {
      const data = yield call(deleteRecord, payload);
      // 刷新列表
      yield put({
        type: 'getRemote',
      });
    },
  },
  reducers: {
    getList(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
    save(state, action) {
      const data = [
        {
          key: '1',
          name: 'John Brown',
          age: 32,
          address: 'New York No. 1 Lake Park',
          tags: ['nice', 'developer'],
        },
        {
          key: '2',
          name: 'Jim Green',
          age: 42,
          address: 'London No. 1 Lake Park',
          tags: ['loser'],
        },
        {
          key: '3',
          name: 'Joe Black',
          age: 32,
          address: 'Sidney No. 1 Lake Park',
          tags: ['cool', 'teacher'],
        },
      ];

      return data;
      // return {
      //   ...state,
      //   ...action.payload,
      // };
    },
    // 启用 immer 之后
    // save(state, action) {
    //   state.name = action.payload;
    // },
  },
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) => {
        if (pathname === '/users') {
          dispatch({
            type: 'getRemote',
          });
        }
      });
    },
  },
};

export default UserModel;

import { SingleUserType } from './type.d';
import request, { extend } from 'umi-request';
import { message } from 'antd';

const errorHandler = function (error: any) {
  if (error.response) {
    if (error.response.status > 400) {
      message.error(error.data.message ? error.data.message : error.data);
    }
    console.log(error.response.status);
    console.log(error.response.headers);
    console.log(error.data);
    console.log(error.request);
  } else {
    console.log(error.message);
    message.error('网络异常,请稍后再试');
  }
  throw error;
};

const extendRequest = extend({ errorHandler });

// 用户列表
export const getRemoteList = async (params: SingleUserType) => {
  try {
    const data = await extendRequest('/api/users', {
      method: 'GET',
      params,
    });
    message.success('列表请求成功');
    return data;
  } catch (error) {
    // message.error(error.message);
    console.log(error);
  }
};

// 用户编辑
export const editRecord = async ({
  id,
  values,
}: {
  id: number;
  values: SingleUserType;
}) => {
  try {
    const data = await request(`/api/users/${id}`, {
      method: 'PUT',
      data: values,
    });
    message.success('编辑成功');
    return data;
  } catch (error) {
    console.log(error);
  }
};

// 添加用户
export const addRecord = async ({ values }: { values: SingleUserType }) => {
  try {
    const data = await request('/api/users', {
      method: 'POST',
      data: values,
    });
    message.success('添加成功');
    return data;
  } catch (error) {
    console.log(error);
  }
};

// 用户删除
export const deleteRecord = async ({ id }: { id: number }) => {
  try {
    const data = await request(`/api/users/${id}`, {
      method: 'DELETE',
    });
    message.success('删除成功');
    return data;
  } catch (error) {
    console.log(error);
  }
};

import { Form, Input, Modal } from 'antd';
import React, { useEffect } from 'react';
import { SingleUserType } from '../type.d';

type Props = {
  visible: boolean;
  handleColse: () => void;
  record: SingleUserType | undefined;
  onFinish: (values: SingleUserType) => void;
  onFinishFailed: (errorInfo: any) => void;
};

const UserModal = (props: Props) => {
  const { visible, handleColse, record, onFinish, onFinishFailed } = props;
  const [form] = Form.useForm();
  useEffect(() => {
    if (record === undefined) {
      form.resetFields();
    } else {
      form.setFieldsValue(record);
    }
  }, [visible]);

  const handleOk = () => {
    form.submit();
  };

  return (
    <div>
      <Modal
        title="Basic Modal"
        visible={visible}
        onCancel={handleColse}
        onOk={handleOk}
      >
        <Form
          name="basic"
          labelCol={{ span: 5 }}
          form={form}
          initialValues={{}}
          onFinish={onFinish}
          onFinishFailed={onFinishFailed}
        >
          <Form.Item
            label="Name"
            name="name"
            rules={[{ required: true, message: 'Please Input your name!' }]}
          >
            <Input />
          </Form.Item>
          <Form.Item label="Email" name="email">
            <Input />
          </Form.Item>
          <Form.Item label="Create Time" name="create_time">
            <Input />
          </Form.Item>
          <Form.Item label="Status" name="status">
            <Input />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
};

export default UserModal;

export interface SingleUserType {
  id: number;
  name: string;
  create_time: string;
  update_time: string;
  status: number;
}

export interface UserModelState {
  data?: SingleUserType[];
  meta?: {
    total: number;
    per_page: number;
    page: number;
  };
}

使用 Pro-table 组件

  • 安装 yarn add @ant-design/pro-table"@2.2.7 moment

RUNOOB 图标

import React, { FC, useRef } from 'react';
import { Button, Popconfirm, Space, Table, Pagination, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { connect, Dispatch } from 'dva';
import { Loading } from '@@/plugin-dva/connect';
import ProTable, { ProColumns, TableDropdown } from '@ant-design/pro-table';
import UserModal from './components/UserModal';
import { SingleUserType, UserModelState } from './type';
import { getRemoteList, editRecord, addRecord } from './service';
// 可以使用 hook 来取代connect
// import { useDispatch, useSelector } from 'react-redux';

interface ActionType {
  reload: () => void;
  fetchMore: () => void;
  reset: () => void;
}

interface UserPageProps {
  pros: UserModelState;
  dispatch: Dispatch;
  useListLoading: boolean;
}

const ProsPageList: FC<UserPageProps> = ({
  pros,
  dispatch,
  useListLoading,
}) => {
  // const { data } = useSelector((state: any) => state.pros);
  // const dispatch = useDispatch();
  const [modalVisible, setModalVisible] = React.useState(false);
  const [record, setRecord] = React.useState<SingleUserType | undefined>(
    undefined,
  );
  const [confirmLoading, setConfirmLoading] = React.useState(false);
  // const ref = useRef<ActionType>();
  const columns: ProColumns<SingleUserType>[] = [
    {
      title: 'ID',
      dataIndex: 'id',
      valueType: 'digit',
      key: 'id',
    },
    {
      title: 'Name',
      dataIndex: 'name',
      key: 'name',
      valueType: 'text',
      render: (text: any) => <a>{text}</a>,
    },
    {
      title: 'CreateTime',
      dataIndex: 'create_time',
      valueType: 'dateTime',
      key: 'create_time',
    },

    {
      title: 'Action',
      key: 'action',
      render: (text: any, record: SingleUserType) => (
        <Space size="middle">
          <a onClick={() => handleEdit(record)}>Edit</a>
          <a>
            <Popconfirm
              title="是否要删除这条记录?"
              onConfirm={() => comform(record)}
            >
              Delete
            </Popconfirm>
          </a>
        </Space>
      ),
    },
  ];
  // 编辑
  const handleEdit = (record: SingleUserType) => {
    setModalVisible(true);
    setRecord(record);
  };
  // 关闭弹窗
  const handleColse = () => {
    setModalVisible(false);
    setRecord(undefined);
  };
  // 编辑提交
  const onFinish = async (values: SingleUserType) => {
    setConfirmLoading(true);
    let id = 0;
    if (record) {
      id = record.id;
    }
    let serviceFun;
    if (id) {
      serviceFun = editRecord;
    } else {
      serviceFun = addRecord;
    }

    const result = await serviceFun({ id, values });
    if (result) {
      setModalVisible(false);
      message.success(`${id === 0 ? 'Add' : 'Edit'} Successfully.`);
      reloadHandler();
      setConfirmLoading(false);
    } else {
      setConfirmLoading(false);
      message.error(`${id === 0 ? 'Add' : 'Edit'} Failed.`);
    }
  };
  const handleAdd = () => {
    setModalVisible(true);
    setRecord(undefined);
  };
  // 删除
  const comform = async (record: SingleUserType) => {
    dispatch({
      type: 'pros/del',
      payload: {
        id: record.id,
      },
    });
  };
  // 提交失败
  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };

  const handleRequest = async ({
    pageSize = 5,
    current = 1,
  }: {
    pageSize: number;
    current: number;
  }) => {
    const pros = await getRemoteList({
      page: current,
      per_page: pageSize,
    });
    console.log('pros', pros.data);
    return {
      data: pros.data,
      success: true,
      total: pros?.meta?.total,
    };
  };

  const paginationHandler = (page: number, pageSize?: number) => {
    dispatch({
      type: 'pros/getRemote',
      payload: {
        page,
        per_page: pageSize ? pageSize : pros?.meta?.per_page,
      },
    });
  };

  const pageSizeHandler = (current: number, size: number) => {
    dispatch({
      type: 'pros/getRemote',
      payload: {
        page: current,
        per_page: size,
      },
    });
  };

  // 刷新
  const reloadHandler = () => {
    dispatch({
      type: 'pros/getRemote',
      payload: {
        page: pros?.meta?.page,
        per_page: pros?.meta?.per_page,
      },
    });
  };
  return (
    <div className="list-table">
      {/* <ProTable
        actionRef={ref}
        loading={useListLoading}
        columns={columns}
        pagination={false}
        rowKey="id"
        search={false}
        request={handleRequest}
      /> */}
      <ProTable
        loading={useListLoading}
        columns={columns}
        pagination={false}
        rowKey="id"
        search={false}
        dataSource={pros?.data}
        options={{
          density: true,
          fullScreen: true,
          reload: () => {
            reloadHandler();
          },
          setting: true,
        }}
        headerTitle="User List"
        toolBarRender={() => [
          <Button type="primary" onClick={handleAdd}>
            Add
          </Button>,
          <Button onClick={reloadHandler}>Reload</Button>,
        ]}
      />
      <Pagination
        className="list-page"
        total={pros?.meta?.total}
        onChange={paginationHandler}
        onShowSizeChange={pageSizeHandler}
        current={pros?.meta?.page}
        pageSize={pros?.meta?.per_page}
        showSizeChanger
        showQuickJumper
        showTotal={(total) => `Total ${total} items`}
      />
      <UserModal
        visible={modalVisible}
        handleColse={handleColse}
        record={record}
        confirmLoading={confirmLoading}
        onFinish={(values) => {
          onFinish(values);
        }}
        onFinishFailed={(errorInfo) => {
          onFinishFailed(errorInfo);
        }}
      />
    </div>
  );
};

const mapStateToProps = ({ pros, loading }: { pros: []; loading: Loading }) => {
  console.log('pros', pros);
  return {
    pros,
    useListLoading: loading.models.pros,
  };
};

export default connect(mapStateToProps)(ProsPageList);

import { Effect, Subscription } from 'dva';
import { Reducer } from 'redux';
import { getRemoteList, editRecord, deleteRecord, addRecord } from './service';
import { UserModelState } from './type';

export interface UserModelType {
  namespace: 'pros';
  state: UserModelState;
  effects: {
    query: Effect;
    getRemote: Effect;
    edit: Effect;
    del: Effect;
    add: Effect;
  };
  reducers: {
    save: Reducer;
    getList: Reducer<UserModelState>;
    // 启用 immer 之后
    // save: ImmerReducer<UserModelState>;
  };
  subscriptions: { setup: Subscription };
}

const UserModel: UserModelType = {
  namespace: 'pros',

  state: {
    data: [],
    meta: {
      total: 0,
      per_page: 5,
      page: 1,
    },
  },

  effects: {
    *query({ payload }, { call, put }) {
      console.log('query');
    },
    // 获取远程数据
    *getRemote({ payload }, { call, put }): any {
      const data = yield call(getRemoteList, payload);
      yield put({
        type: 'getList',
        payload: data,
      });
    },
    // 编辑用户
    *edit({ payload }, { call, put, select }): any {
      const data = yield call(editRecord, payload);
      // 刷新列表
      const { page, per_page } = yield select((state: any) => state.users.meta);
      // 刷新列表
      yield put({
        type: 'getRemote',
        payload: {
          page,
          per_page,
        },
      });
    },
    //添加用户
    *add({ payload }, { call, put, select }): any {
      const data = yield call(addRecord, payload);
      // 刷新列表
      const { page, per_page } = yield select((state: any) => state.users.meta);
      // 刷新列表
      yield put({
        type: 'getRemote',
        payload: {
          page,
          per_page,
        },
      });
    },
    // 删除用户
    *del({ payload }, { call, put, select }): any {
      const data = yield call(deleteRecord, payload);
      const { page, per_page } = yield select((state: any) => state.users.meta);
      // 刷新列表
      yield put({
        type: 'getRemote',
        payload: {
          page,
          per_page,
        },
      });
    },
  },
  reducers: {
    getList(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
    save(state, action) {
      const data = [
        {
          key: '1',
          name: 'John Brown',
          age: 32,
          address: 'New York No. 1 Lake Park',
          tags: ['nice', 'developer'],
        },
        {
          key: '2',
          name: 'Jim Green',
          age: 42,
          address: 'London No. 1 Lake Park',
          tags: ['loser'],
        },
        {
          key: '3',
          name: 'Joe Black',
          age: 32,
          address: 'Sidney No. 1 Lake Park',
          tags: ['cool', 'teacher'],
        },
      ];

      return data;
      // return {
      //   ...state,
      //   ...action.payload,
      // };
    },
    // 启用 immer 之后
    // save(state, action) {
    //   state.name = action.payload;
    // },
  },
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) => {
        if (pathname === '/pros') {
          dispatch({
            type: 'getRemote',
            payload: {
              page: 1,
              per_page: 5,
            },
          });
        }
      });
    },
  },
};

export default UserModel;

import { SingleUserType } from './type';
import request, { extend } from 'umi-request';
import { message } from 'antd';

const errorHandler = function (error: any) {
  if (error.response) {
    if (error.response.status > 400) {
      message.error(error.data.message ? error.data.message : error.data);
    }
    console.log(error.response.status);
    console.log(error.response.headers);
    console.log(error.data);
    console.log(error.request);
  } else {
    console.log(error.message);
    message.error('网络异常,请稍后再试');
  }
  throw error;
};

const extendRequest = extend({ errorHandler });

// 用户列表
export const getRemoteList = async ({
  page,
  per_page,
}: {
  page: number;
  per_page: number;
}) => {
  try {
    const data = await extendRequest(
      `/api/users?page=${page}&per_page=${per_page}`,
      {
        method: 'GET',
      },
    );
    message.success('列表请求成功');
    return data;
  } catch (error) {
    // message.error(error.message);
    console.log(error);
  }
};

// 用户编辑
export const editRecord = async ({
  id,
  values,
}: {
  id: number;
  values: SingleUserType;
}) => {
  try {
    const data = await request(`/api/users/${id}`, {
      method: 'PUT',
      data: values,
    });
    message.success('编辑成功');
    console.log('data', data);
    return true;
  } catch (error) {
    console.log(error);
    return false;
  }
};

// 添加用户
export const addRecord = async ({ values }: { values: SingleUserType }) => {
  try {
    const data = await request('/api/users', {
      method: 'POST',
      data: values,
    });
    message.success('添加成功');
    console.log('data', data);
    return true;
  } catch (error) {
    return false;
    console.log(error);
  }
};

// 用户删除
export const deleteRecord = async ({ id }: { id: number }) => {
  try {
    const data = await request(`/api/users/${id}`, {
      method: 'DELETE',
    });
    message.success('删除成功');
    return data;
  } catch (error) {
    console.log(error);
  }
};

import { DatePicker, Form, Input, Modal, Switch } from 'antd';
import moment from 'moment';
import React, { useEffect } from 'react';
import { SingleUserType } from '../type';

type Props = {
  visible: boolean;
  handleColse: () => void;
  record: SingleUserType | undefined;
  onFinish: (values: SingleUserType) => void;
  onFinishFailed: (errorInfo: any) => void;
  confirmLoading: boolean;
};

const UserModal = (props: Props) => {
  const {
    visible,
    handleColse,
    record,
    onFinish,
    onFinishFailed,
    confirmLoading,
  } = props;
  const [form] = Form.useForm();
  useEffect(() => {
    if (record === undefined) {
      form.resetFields();
    } else {
      form.setFieldsValue({
        ...record,
        create_time: moment(record.create_time),
        status: Boolean(record.status),
      });
    }
  }, [visible]);

  const handleOk = () => {
    form.submit();
  };

  return (
    <div>
      <Modal
        title={record ? 'Edit ID: ' + record.id : 'Add'}
        visible={visible}
        onCancel={handleColse}
        onOk={handleOk}
        confirmLoading={confirmLoading}
      >
        <Form
          name="basic"
          labelCol={{ span: 5 }}
          form={form}
          initialValues={{}}
          onFinish={onFinish}
          onFinishFailed={onFinishFailed}
        >
          <Form.Item
            label="Name"
            name="name"
            rules={[{ required: true, message: 'Please Input your name!' }]}
          >
            <Input />
          </Form.Item>
          <Form.Item label="Email" name="email">
            <Input />
          </Form.Item>
          <Form.Item label="Create Time" name="create_time">
            <DatePicker showTime />
          </Form.Item>
          <Form.Item label="Status" name="status" valuePropName="checked">
            <Switch />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
};

export default UserModal;

参考资料