tio-boot 案例 - 整合 ant design pro 增删改查

前端界面

data.d.ts

// data.d.ts
export interface Post {
  id: string;
  user_id: string;
  title: string;
  content: string;
  attached_images: string[];
  images: string[];
  share_num: number;
  like_num: number;
  review_num: number;
  created_at: string;
}

export interface PagePost {
  data: any;
  total: number;
  list: Post[];
}

service.ts

// service.ts
import {request} from '@umijs/max';
import type {Post, PagePost} from './data.d';

export async function page(data: any) {
  return request<PagePost>('/api/posts/page', {
    method: 'POST',
    data,
  });
}

export async function createPost(data: Post) {
  return request<Post>('/api/posts/create', {
    method: 'POST',
    data,
  });
}


export async function deletePost(id: string) {
  return request<void>(`/api/posts/delete/${id}`, {
    method: 'GET',
  });
}

export async function uploadFile(file: any) {
  const formData = new FormData();
  formData.append('file', file);
  return request('/api/system/file/uploadImageToGoogle', {
    method: 'POST',
    data: formData,
  });
}

column.tsx

import React from 'react';
import {ProColumns} from '@ant-design/pro-components';
import {Upload} from 'antd';
import type {Post} from './data.d'; // Adjust the import path as necessary

export const postColumns = (
  handleShowDetail: (record: Post) => void,
  handleDelete: (id: string) => Promise<void>,
  handleShowEditModal: (record: Post) => void,
): ProColumns<Post>[] => [
  {
    title: 'Id',
    dataIndex: 'id',
    render: (dom, entity) => {
      return (
        <a
          onClick={() => handleShowDetail(entity)}
        >
          {dom}
        </a>
      );
    },
  },
  {
    title: 'Title',
    dataIndex: 'title',
  },
  {
    title: 'Content',
    dataIndex: 'content',
  },
  {
    title: 'Share Count',
    dataIndex: 'share_num',
  },
  {
    title: 'Like Count',
    dataIndex: 'like_num',
  },
  {
    title: 'Review Count',
    dataIndex: 'review_num',
  },
  {
    title: 'Attached Images',
    dataIndex: 'attached_images',
    search: false,
    render: (_, record) => (
      <Upload
        listType="picture-card"
        fileList={
          (record.attached_images || []).map((url) => ({
            uid: url,
            name: 'image',
            status: 'done',
            url,
          }))
        }
      />
    ),
  },
  {
    title: 'Operation',
    valueType: 'option',
    render: (text, record) => [
      <a key="delete" onClick={() => handleDelete(record.id)}>Delete</a>,
      <a key="edit" onClick={() => handleShowEditModal(record)}>Edit</a>,
    ],
  },
];

PostCreateForm.tsx

import React, {useState} from 'react';
import {ModalForm, ProFormList, ProFormText, ProFormTextArea, ProFormUploadButton} from '@ant-design/pro-components';
import {Button, Form, Image, Upload} from 'antd';
import {UploadFile, UploadProps} from 'antd/lib/upload/interface';
import {PlusOutlined} from "@ant-design/icons";
import {uploadFile} from '../service';


type CreateFormProps = {
  visible: boolean;
  title: string;
  formData: any;
  onVisibleChange: (visible: boolean) => void;
  onFinish: (value: any) => Promise<boolean>;
};


const PostCreateForm: React.FC<CreateFormProps> = ({
                                                     visible,
                                                     title,
                                                     formData,
                                                     onVisibleChange,
                                                     onFinish,
                                                   }) => {
  const [form] = Form.useForm();
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  const [previewImage, setPreviewImage] = useState('');
  const [previewOpen, setPreviewOpen] = useState(false);
  const [imageIds, setImageIds] = useState([]);
  const [imageUrls, setImageUrls] = useState([]);


  const getBase64 = (file: Blob): Promise<string> =>
    new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => resolve(reader.result as string);
      reader.onerror = (error) => reject(error);
    });

  const handlePreview = async (file: UploadFile) => {
    if (!file.url && !file.preview) {
      file.preview = await getBase64(file.originFileObj as Blob);
    }
    setPreviewImage(file.url || (file.preview as string));
    setPreviewOpen(true);
  };



  const handleChange: UploadProps['onChange'] = ({fileList: newFileList}) => {
    // Assuming setFileList is defined elsewhere to update the state for fileList
    setFileList(newFileList);

    // Extracting 'id' from each file's response.data and appending to imageIds
    const newIds = newFileList.map(file => file.response?.data.id).filter(id => id !== undefined);
    const imageUrls = newFileList.map(file => file.response?.data.url).filter(url => url !== undefined);
    // Updating imageIds state by appending newIds
    // @ts-ignore
    setImageIds(newIds);
    // @ts-ignore
    setImageUrls(imageUrls);
  }


  const uploadButton = (
    <div>
      <PlusOutlined/>
      <div style={{marginTop: 8}}>Upload</div>
    </div>
  );

  const debug = () => {
    console.log("imageIds", imageIds)
  }
  const debugButton = (
    <Button type="primary" onClick={debug}>
      Debug
    </Button>
  );

  const customUploadRequest = (options: any) => {
    const {file, onSuccess, onError} = options;
    uploadFile(file)
      .then(response => {
        //必须回调
        onSuccess(response, file);
      })
      .catch(onError); // 上传失败
  };

  const onFormVisibleChange = (newVisible: boolean) => {
    onVisibleChange(newVisible);
    if (newVisible) {
      form.setFieldsValue(formData || {});
      if(formData){
        let attachedImages = formData['attached_images'];

        let fileList:UploadFile[] = [];

        for (const url of attachedImages) {
          fileList.push({
            uid: url,
            name: 'image',
            status: 'done',
            url
          });
        }
        setFileList(fileList);
      }

    } else {
      form.resetFields();
      setFileList([]);
    }
  };

  const onFormFinish = async (formValues: any) => {
    // 将imageIds添加到表单提交的数据中
    const submissionData = {
      ...formValues,
      attached_images: imageUrls,
      idType:'long',
      like_numType:'long',
      review_numType:'long',
      share_numType:'long'
    };
    if(await onFinish(submissionData)){
      form.resetFields();
      setFileList([]);
    }

  };

  return (
    <ModalForm
      title={title}
      visible={visible}
      onVisibleChange={onFormVisibleChange}
      onFinish={onFormFinish}
      form={form}
    >
      <ProFormText
        name="id"
        label="id"
        hidden
      />

      <ProFormText
        rules={[{required: true, message: 'Required'}]}
        name="title"
        label="Title"
      />
      <ProFormTextArea
        rules={[{required: true, message: 'Required'}]}
        name="content"
        label="Content"
      />
      <ProFormText
        name="share_num"
        label="share num"
      />
      <ProFormText
        name="like_num"
        label="like num"
      />
      <ProFormText
        name="review_num"
        label="review num"
      />


      <Upload
        listType="picture-card"
        fileList={fileList}
        onPreview={handlePreview}
        onChange={handleChange}
        customRequest={customUploadRequest}
      >
        {fileList.length >= 8 ? null : uploadButton}
      </Upload>
      {previewImage && (
        <Image
          wrapperStyle={{display: 'none'}}
          preview={{
            visible: previewOpen,
            onVisibleChange: setPreviewOpen,
            afterOpenChange: (visible) => !visible && setPreviewOpen(false),
          }}
          src={previewImage}
        />
      )}

      {/*{debugButton}*/}
    </ModalForm>
  )
}
export default PostCreateForm;

index.tsx

import React, {useRef, useState} from 'react';
import type {ActionType, ProColumns} from '@ant-design/pro-components';
import {ProDescriptions, ProDescriptionsItemProps, ProTable} from '@ant-design/pro-components';
import {Button, Drawer, message, Upload} from 'antd';
import {PlusOutlined} from "@ant-design/icons";
import type {Post} from './data.d';
import {createPost, deletePost, page, uploadFile} from './service';
import PostCreateForm from "@/pages/business/posts/components/PostCreateForm";
import {postColumns} from "@/pages/business/posts/column";

const handleAdd = async (fields: Post) => {
  const hide = message.loading('loading...');

  try {
    await createPost({...fields});
    hide();
    message.success('success');
    return true;
  } catch (error) {
    hide();
    message.error('fail!');
    return false;
  }
};


const PostManagement: React.FC = () => {
  const [editingPost, setEditingPost] = useState<Post | null>(null);
  const actionRef = useRef<ActionType>();
  const [showDetail, setShowDetail] = useState<boolean>(false);
  const [currentRow, setCurrentRow] = useState<Post>();
  const [formTitle, setFormTitle] = useState<string>('');
  /** 新建窗口的弹窗 */
  const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);

  const handleModalVisible = (visible: boolean) => {
    setCreateModalVisible(visible);
  };

  const [modalVisible, setModalVisible] = useState(false);

  const handleModalClose = () => {
    setModalVisible(false);
  };


  const handleDelete = async (id: string) => {
    try {
      await deletePost(id);
      message.success('Post deleted successfully');
      actionRef.current?.reload();
    } catch (error) {
      message.error('Failed to delete post');
    }
  };


  const handleCreate = () => {
    setModalVisible(true);
    setEditingPost(null)
    setFormTitle("New")
  };

  const handleShowEditModal = (record: Post) => {
    setEditingPost(record);
    setModalVisible(true);
    setFormTitle("Edit")
  };

  const handleShowDetail = (entity: Post) => {
    setCurrentRow(entity);
    setShowDetail(true);
  };


  let toolBars = [
    <Button icon={<PlusOutlined/>} type="primary" onClick={handleCreate}>
      New
    </Button>,
  ];

  const columns = postColumns(handleShowDetail, handleDelete, handleShowEditModal);

  const onFinishForm = async (value: Post) => {
    const success = await handleAdd(value);
    if (success) {
      handleModalClose();
      if (actionRef.current) {
        actionRef.current.reload(); //重新加载数据
      }
    }
    return success;
  };

  return (
    <>
      <ProTable<Post>
        columns={columns}
        actionRef={actionRef}
        request={async (params) => {
          params.idType = 'long';
          params.like_numType = 'long';
          params.review_numType = 'long';
          params.share_numType = 'long';
          params.titleOp = "ct";
          params.contentOp = "ct";
          params.orderBy = "created_at";
          params.isAsc = "false";

          const response = await page(params);
          return {
            data: response.data.list,
            success: true,
            total: response.data.total,
          };
        }}
        rowKey="id"
        search={{
          labelWidth: 'auto',
          defaultCollapsed: false,
        }}
        form={{initialValues: {pageSize: 10}}}
        pagination={{
          pageSize: 10,
        }}
        toolBarRender={() => toolBars}
      >
      </ProTable>
      <Drawer
        width={600}
        open={showDetail}
        onClose={() => {
          setCurrentRow(undefined);
          setShowDetail(false);
        }}
        closable={false}
      >
        {currentRow?.id && (
          <ProDescriptions<Post>
            column={2}
            title={currentRow?.id}
            request={async () => ({
              data: currentRow || {},
            })}
            params={{
              id: currentRow?.id,
            }}
            columns={columns as ProDescriptionsItemProps<Post>[]}
          />
        )}
      </Drawer>
      <PostCreateForm
        title={formTitle}
        visible={modalVisible}
        onVisibleChange={setModalVisible}
        onFinish={onFinishForm}
        formData={editingPost}
      />
    </>
  );
};

export default PostManagement;