Tabel组件支持层级展开、拖拽排序,Tabel的样式设置等

99 阅读3分钟
import empty from '@/Images/others/empty.png';
import { ProductType } from '@/modelType/ProductCatalog';
import type { InputRef } from 'antd';
import { Input, Switch, Table } from 'antd';
import type { FormInstance } from 'antd/es/form';
import { connect } from 'dva';
import React, {
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState
} from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { UNVForm, UNVIcon, UNVLoading } from 'UNV-DESIGN';
import CatalogConfig from './components/CatalogConfig';
import {
  dataType,
  findFromData,
  getParam,
  ItemTypes,
  optionsTyps
} from './components/DragBodyRow/DragUtils';
import Headers from './components/Headers';
import styles from './index.less';

const EditableContext = React.createContext<FormInstance<any> | null>(null);

interface DraggableBodyRowProps
  extends React.HTMLAttributes<HTMLTableRowElement> {
  index: number;
  moveRow: (record: { [key: string]: any }) => void;
  [key: string]: any;
}

//#region 拖拽行
const DraggableBodyRow = (props: DraggableBodyRowProps) => {
  const {
    record,
    data,
    index,
    className,
    style,
    moveRow,
    findRow,
    ...restProps
  } = props;

  if (!record) {
    return null;
  }

  const [form] = UNVForm.useForm();

  const {
    row: originalRow,
    rowIndex: originalIndex,
    rowParentIndex: originalParentIndex
  } = findRow(record.id);

  const itemObj = {
    id: record.id,
    parentId: record.parentCatalogId,
    index,
    isGroup: record.type === dataType.group,
    originalRow, // 拖拽原始数据
    originalIndex, // 拖拽原始数据索引
    originalParentIndex // 拖拽原始数据父节点索引
  };

  const isDrag = true;

  const ref = useRef<HTMLTableRowElement>(null);

  const [{ handlerId, isOver, dropClassName }, drop] = useDrop({
    accept: ItemTypes,
    collect: (monitor: any) => {
      const {
        id: dragId,
        parentId: dragParentId,
        index: dragPreIndex,
        isGroup
      } = monitor.getItem() || {};

      if (dragId === record.id) {
        return {};
      }

      // 是否可以拖拽替换
      let isOver = monitor.isOver();
      if (isGroup) {
        // 要覆盖的数据是分组,或者是最外层的子项可以替换,其他情况不可以
        const recordIsGroup = record.type === dataType.group;
        if (!recordIsGroup) {
          isOver = false;
        }
      } else {
        // 要覆盖的数据是子项,但不在同分组不可以替换
        if (dragParentId !== record.parentCatalogId) {
          isOver = false;
        }
      }

      return {
        isOver,
        dropClassName: 'drop-over-downward',
        handlerId: monitor.getHandlerId()
      };
    },
    hover: (item: any, monitor) => {
      if (!ref.current) {
        return;
      }
      const dragIndex = item.index;
      const dropIndex = index;
      // Don't replace items with themselves
      if (dragIndex === dropIndex) {
        return;
      }

      // let opt = {
      //   dragId: item.id, // 拖拽id
      //   dropId: record.id, // 要放置位置行的id
      //   dropParentId: record.parentCatalogId,
      //   operateType: optionsTyps.hover, // hover操作
      // };

      // moveRow(opt);

      item.index = dropIndex;
    },
    drop: (item) => {
      const opt = {
        dragId: item.id, // 拖拽id
        dropId: record.id, // 要放置位置行的id
        dropParentId: record.parentCatalogId,
        operateType: optionsTyps.drop
      };
      moveRow(opt);
    }
  });

  const [{ isDragging }, drag] = useDrag({
    type: ItemTypes,
    item: itemObj,
    collect: (monitor) => ({
      isDragging: monitor.isDragging()
    }),
    // canDrag: (props, monitor) => isDrag //parentId不为0的才可以拖拽
    end: (item, monitor) => {
      const { id: droppedId, originalRow } = item;
      const didDrop = monitor.didDrop();
      // 超出可拖拽区域,需要将拖拽行还原
      if (!didDrop) {
        const opt = {
          dragId: droppedId, // 拖拽id
          dropId: originalRow.id, // 要放置位置行的id
          dropParentId: originalRow.parentCatalogId,
          originalIndex,
          originalParentIndex,
          operateType: optionsTyps.didDrop
        };
        moveRow(opt);
      }
    }
  });

  drop(drag(ref));

  // 拖拽行的位置显示透明
  const opacity = isDragging ? 0 : 1;

  return (
    <UNVForm form={form} component={false}>
      <EditableContext.Provider value={form}>
        <tr
          ref={ref}
          className={`${className}${isOver ? dropClassName : ''}`}
          style={{ cursor: 'move', ...style }}
          {...restProps}
        />
      </EditableContext.Provider>
    </UNVForm>
  );
};
//#endregion 拖拽行结束

interface EditableCellProps {
  title: React.ReactNode;
  editable: boolean;
  children: React.ReactNode;
  dataIndex: keyof ProductType;
  record: ProductType;
  handleSave: (record: ProductType) => void;
}

//#region 暂时先这么用,提组件有bug,后续解决
const EditTableCell: React.FC<EditableCellProps> = ({
  title,
  editable,
  children,
  dataIndex,
  record,
  handleSave,
  ...restProps
}) => {
  const [editing, setEditing] = useState(false);
  const inputRef = useRef<InputRef>(null);
  const form = useContext(EditableContext)!;

  useEffect(() => {
    if (editing) {
      inputRef.current!.focus();
    }
  }, [editing]);

  const toggleEdit = () => {
    setEditing(!editing);
    form.setFieldsValue({ [dataIndex]: record[dataIndex] });
  };

  const save = async () => {
    try {
      const values = await form.validateFields();

      toggleEdit();
      if (Number(values[dataIndex] !== record[dataIndex])) {
        handleSave({ ...record, ...values });
      }
    } catch (errInfo) {
      console.log('Save failed:', errInfo);
    }
  };

  let childNode = children;

  if (editable) {
    childNode = editing ? (
      <UNVForm.Item
        style={{ margin: 0 }}
        name={dataIndex}
        rules={[
          {
            required: true,
            message: intl.formatMessage({ id: 'Please enter digits' })
          },
          {
            pattern: new RegExp(/^([0-9]){1,}$/),
            message: intl.formatMessage({ id: 'Please enter digits' })
          }
        ]}
      >
        <Input ref={inputRef} onPressEnter={save} onBlur={save} />
      </UNVForm.Item>
    ) : (
      <div
        className="editable-cell-value-wrap"
        style={{ paddingRight: 24 }}
        onClick={toggleEdit}
      >
        {children}
      </div>
    );
  }

  return <td {...restProps}>{childNode}</td>;
};

//#endregion tablecell结束

type EditableTableProps = Parameters<typeof Table>[0];

type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>;

/**
 * 格式化数据
 * @param list
 * @returns 格式化后的数组
 */
const structuredData = (list: any) => {
  const newList = list.map((item: any) => {
    return {
      ...item,
      children:
        item.childList && item.childList.length ?
          structuredData(item.childList) :
          null
    };
  });
  return newList;
};

/**
 * 格式化,隐藏无效数据
 * @param list
 * @returns 格式化后的数组
 */
const structuredHideData = (list: any) => {
  const newList: Array<any> = [];

  const struct = (data: Array<any>, result: Array<any>) => {
    data.forEach((item: any) => {
      if (item.isValid) {
        const newItem = { ...item, children: [] };
        if (newItem.childList && newItem.childList.length > 0) {
          struct(newItem.childList, newItem.children);
        } else {
          newItem.children = null;
        }
        result.push(newItem);
      }
    });
  };

  struct(list, newList);
  // const newList = list.map((item: any) => {
  //   if (item.isValid) {
  //     return {
  //       ...item,
  //       children:
  //         item.childList && item.childList.length
  //           ? structuredHideData(item.childList)
  //           : null,
  //     };
  //   } else {
  //     return {};
  //   }
  // });
  return newList;
};

/**
 * @description  平铺数据
 * @param {any} list 待平铺数据
 * @return 平铺数据
 */
const _structuredAllData = (list: any) => {
  const result: Array<any> = [];

  const trailData = (tempList: Array<any>) => {
    if (Array.isArray(tempList) && tempList.length > 0) {
      tempList.forEach((item: any) => {
        result.push(item);
        if (item.childList && Array.isArray(item.childList)) {
          trailData(item.childList);
        }
      });
    }
  };

  trailData(list);

  return result;
};

const ProductCatalog = (props: { [key: string]: any }) => {
  const { dispatch, queryTreeLoading, productTreeList } = props;

  // table列表的数据源
  const [data, setData] = useState<ProductType[]>([]);

  // 展示筛选配置弹窗
  const [screenModalData, setScreenModalData] = useState<ProductType | {}>({});

  // table默认展开行
  const [expandKeys, setExpandKeys] = useState<React.Key[]>([]);

  // 是否隐藏无效项
  const [hideValid, setHideValid] = useState(false);

  useEffect(() => {
    dispatch({
      type: 'productCatalog/queryProductCatalogTree',
      payload: {}
    });
  }, []);

  useEffect(() => {
    if (Array.isArray(productTreeList)) {
      if (!hideValid) {
        setData(structuredData(productTreeList));
      } else {
        setData(structuredHideData(productTreeList));
      }
    }
  }, [productTreeList, hideValid]);

  const updateCatalog = (obj: {
    id: number;
    isValid?: boolean;
    sort_no?: number;
  }) => {
    dispatch({
      type: 'productCatalog/productCatalog',
      payload: { ...obj, productCatalogCode: 'EXT' }
    });
  };

  const handleSave = (row: ProductType) => {
    // const newData = [...data];
    // const index = newData.findIndex((item) => row.id === item.id);
    // const item = newData[index];
    // newData.splice(index, 1, {
    //   ...item,
    //   ...row,
    // });
    // setData(newData);
    updateCatalog({ id: row.id, sort_no: row.sortNo });
  };

  const findRow = (id: number | string) => {
    const { row, index, parentIndex } = findFromData(data, id);
    return {
      row,
      rowIndex: index,
      rowParentIndex: parentIndex
    };
  };

  //#region 移动目录后逻辑处理
  /**
   * 移动目录后逻辑处理
   */
  const moveRow = useCallback(
    (props: any) => {
      const { dragId, dropId, dropParentId, operateType, originalIndex } =
        props;

      const {
        dragRow,
        dropRow,
        dragIndex = 0,
        dropIndex = 0,
        dragParentIndex, // 拖拽子节点的父节点索引
        dropParentIndex, // 放置子节点父节点索引
        dragTopLevelIndex, // 拖拽的顶级节点索引
        dropTopLevelIndex // 放置的顶级节点索引
      } = getParam(data, dragId, dropId);

      // 拖拽是否是组
      const dragIsGroup =
        dragRow.type === dataType.group || !dragRow.parentCatalogId;
      // 放置的是否是组
      const dropIsGroup = !dropParentId;

      // 根据变化的数据查找拖拽行的row和索引
      const {
        row,
        index: rowIndex = 0,
        parentIndex: rowParentIndex
      } = findFromData(data, dragId);

      const newData = data;
      // 组拖拽
      if (dragIsGroup && dropIsGroup) {
        // 超出出拖拽区域还原
        if (operateType === optionsTyps.didDrop) {
          // 【暂留】超出拖拽区域
          // newData = update(data, {
          //   $splice: [
          //     [rowIndex, 1], //删除目前拖拽的索引的数据
          //     [originalIndex, 0, row], // 将拖拽数据插入原始索引位置
          //   ],
          // });
        } else if (dragIndex !== dropIndex) {
          // newData = update(data, {
          //   $splice: [
          //     [dragIndex, 1],
          //     [dropIndex, 0, dragRow],
          //   ],
          // });
          dispatch({
            type: 'productCatalog/updateProductCatalogSortNoByDrag',
            payload: {
              dragType: dragIndex < dropIndex ? 0 : 1,
              productCatalogId: dragRow.id,
              targetProductCatalogId: dropRow.id
            }
          });
        }
      } else if (dragRow.parentCatalogId === dropRow?.parentCatalogId) {
        // 同一组下的子项拖拽
        if (
          dragTopLevelIndex === dropTopLevelIndex &&
          typeof dragTopLevelIndex === 'number' &&
          typeof dropTopLevelIndex === 'number'
        ) {
          // 超出拖拽区域还原
          if (operateType === optionsTyps.didDrop) {
            // 【暂留】超出拖拽区域
            // newData = update(data, {
            //   [dragTopLevelIndex]: {
            //     children: {
            //       [dragParentIndex]: {
            //         children: {
            //           $splice: [
            //             [rowIndex, 1],
            //             [originalIndex, 0, row],
            //           ],
            //         },
            //       },
            //     },
            //   },
            // });
          } else if (dragIndex !== dropIndex) {
            // newData = update(data, {
            //   [dragTopLevelIndex]: {
            //     children: {
            //       [dragParentIndex]: {
            //         children: {
            //           $splice: [
            //             [dragIndex, 1],
            //             [dropIndex, 0, dragRow],
            //           ],
            //         },
            //       },
            //     },
            //   },
            // });
            dispatch({
              type: 'productCatalog/updateProductCatalogSortNoByDrag',
              payload: {
                dragType: dragIndex < dropIndex ? 0 : 1,
                productCatalogId: dragRow.id,
                targetProductCatalogId: dropRow.id
              }
            });
          }
        } else if (!dropTopLevelIndex && !dragTopLevelIndex) {
          // 超出拖拽区域还原
          if (operateType === optionsTyps.didDrop) {
            // 【暂留】超出拖拽区域
            // newData = update(data, {
            //   [dragParentIndex]: {
            //     children: {
            //       $splice: [
            //         [rowIndex, 1],
            //         [originalIndex, 0, row],
            //       ],
            //     },
            //   },
            // });
          } else if (dragIndex !== dropIndex) {
            // newData = update(data, {
            //   [dragParentIndex]: {
            //     children: {
            //       $splice: [
            //         [dragIndex, 1],
            //         [dropIndex, 0, dragRow],
            //       ],
            //     },
            //   },
            // });
            dispatch({
              type: 'productCatalog/updateProductCatalogSortNoByDrag',
              payload: {
                dragType: dragIndex < dropIndex ? 0 : 1,
                productCatalogId: dragRow.id,
                targetProductCatalogId: dropRow.id
              }
            });
          }
        }
      }
    },
    [data]
  );
  //#endregion

  const showScreen = (record: ProductType) => {
    setScreenModalData(record);
  };

  const defaultColumns: (ColumnTypes[number] & {
    editable?: boolean;
    dataIndex: string;
  })[] = [
    {
      title: intl.formatMessage({ id: 'Name1' }),
      align: 'left',
      dataIndex: 'productCatalogName',
      width: 380,
      ellipsis: true
    },
    {
      title: intl.formatMessage({ id: 'Level1' }),
      align: 'center',
      dataIndex: 'catalogLevel',
      render: (text: number) => {
        switch (text) {
          case 1:
            return intl.formatMessage({ id: 'Level I' });
          case 2:
            return intl.formatMessage({ id: 'Level II' });
          case 3:
            return intl.formatMessage({ id: 'Level III' });
          default:
            return intl.formatMessage({ id: 'N/A' });
        }
      }
    },
    {
      title: intl.formatMessage({ id: 'Order' }),
      align: 'center',
      dataIndex: 'sortNo',
      editable: true
    },
    // {
    //   title: '备注',
    //   align: 'center',
    //   dataIndex: '',
    //   width: 340,
    //   ellipsis: true,
    // },
    {
      title: intl.formatMessage({ id: 'Display on UI' }),
      align: 'center',
      dataIndex: 'isValid',
      render: (isValid: boolean, record: ProductType) => (
        <Switch
          checked={isValid}
          onChange={() => {
            updateCatalog({ id: record.id, isValid: !isValid });
          }}
        />
      )
    },
    {
      title: intl.formatMessage({ id: 'Advanced Filter Config' }),
      align: 'center',
      dataIndex: 'operation',
      render: (_: any, record: ProductType) => (
        <UNVIcon
          icon="configIcon"
          onClick={() => {
            showScreen(record);
          }}
        />
      )
    }
  ];

  const columns = defaultColumns.map((col) => {
    if (!col.editable) {
      return col;
    }
    return {
      ...col,
      onCell: (record: ProductType) => ({
        record,
        editable: col.editable,
        dataIndex: col.dataIndex,
        title: col.title,
        handleSave
      })
    };
  });


  return (
    <div className={styles.productCatalog_index_container}>
      <UNVLoading loading={queryTreeLoading}>
        <div className={styles.productCatalog_index_title}>
          {intl.formatMessage({ id: 'DIMS Product Catalog' })}
        </div>
        <Headers
          onFirstAction={() => {
            setExpandKeys(_structuredAllData(data).map((item: any) => item.id));
          }}
          onSecondAction={() => {
            setExpandKeys([]);
          }}
          onThirdAction={() => {
            dispatch({
              type: 'productCatalog/resetProductCatalogSortNo',
              payload: {}
            });
          }}
          check={hideValid}
          setCheck={setHideValid}
        />
        <div className={styles.productCatalog_index_content}>
          <DndProvider backend={HTML5Backend}>
            <Table
              size="small"
              rowKey="id"
              columns={columns as ColumnTypes}
              dataSource={data}
              bordered
              pagination={false}
              scroll={{ y: 500 }}
              components={{
                body: {
                  row: DraggableBodyRow,
                  cell: EditTableCell
                }
              }}
              onRow={(record: any, index: number) => ({
                record,
                data,
                index,
                moveRow,
                findRow
              })}
              expandable={{
                expandedRowKeys: expandKeys,
                onExpand: (_: boolean, row: ProductType) => {
                  setExpandKeys(() => {
                    if (expandKeys.includes(row.id)) {
                      return expandKeys.filter(
                        (item: React.Key) => item !== row.id
                      );
                    }
                    return [...expandKeys, row.id];
                  });
                }
              }}
              locale={{
                emptyText: (
                  <div
                    className="empty"
                    style={{ height: '100%', paddingTop: '10%' }}
                  >
                    <img src={empty} />
                    <div>{intl.formatMessage({ id: 'No data' })}</div>
                  </div>
                )
              }}
            />
          </DndProvider>
        </div>
      </UNVLoading>
    </div>
  );
};

export default connect(
  ({ productCatalog, loading }: { [key: string]: any }) => {
    const { productTreeList } = productCatalog;
    return {
      productTreeList,
      queryTreeLoading:
        loading.effects['productCatalog/queryProductCatalogTree'] || false
    };
  }
)(ProductCatalog);

table样式设置

 :global {
    .ant4-modal-footer {
      margin-top: -20px;
      .ant-spin-nested-loading,
      .ant-spin-container,
      .ant-table,
      .ant-table-container,
      .ant-table-content {
        width: 100%;
        height: 100%;
      }
      .ant-table-container {
        background-color: #f7f8fa;
      }
      .ant-table-row {
        background-color: white;
      }
    }
  }