IdbI

第77章—低代码编辑器:拖拽优化、Table组件

amis 编辑器里,物料拖动到画布区后,还可以拖动改变位置:

现在我们的编辑器没有支持拖动改变位置:

我们来实现下:

其实这个也很简单,就是给物料也加上 useDrag 就可以了。

比如给 Button 加一下:

const [_, drag] = useDrag({
  type: 'Button',
  item: {
      type: 'Button'
  }
});

现在是能拖动了,但是和从物料区拖过来的 drop 逻辑一样,都是新增组件。

我们得区分下两者。

加上 dragType 属性,然后带上当前拖拽的组件 id:

在 useDrop 的时候判断下 dragTag,如果是 move,那就先 delete 再 add

import { useDrop } from "react-dnd";
import { useComponentConfigStore } from "../stores/component-config";
import { getComponentById, useComponetsStore } from "../stores/components";

export interface ItemType {
  type: string;
  dragType?: 'move' | 'add',
  id: number
}

export function useMaterailDrop(accept: string[], id: number) {
    const { addComponent, deleteComponent, components } = useComponetsStore();
    const { componentConfig } = useComponentConfigStore();

    const [{ canDrop }, drop] = useDrop(() => ({
        accept,
        drop: (item: ItemType, monitor) => {
            const didDrop = monitor.didDrop()
            if (didDrop) {
              return;
            }

            if(item.dragType === 'move') {
              const component = getComponentById(item.id, components)!;

              deleteComponent(item.id);

              addComponent(component, id)
            } else {
              const config = componentConfig[item.type];

              addComponent({
                id: new Date().getTime(),
                name: item.type,
                desc: config.desc,
                props: config.defaultProps
              }, id)
            }
        },
        collect: (monitor) => ({
          canDrop: monitor.canDrop(),
        }),
    }));

    return { canDrop, drop }
}

测试下:

这样就实现了拖拽改变位置。

在 Container 组件也加上 useDrag:

这里因为要同时给 div 绑定 drag、drop 的处理,所以用 useRef 拿到 ref 之后再绑定。

import { useDrag } from 'react-dnd';
import { useMaterailDrop } from '../../hooks/useMaterailDrop';
import { CommonComponentProps } from '../../interface';
import { useEffect, useRef } from 'react';

const Container = ({ id, name, children, styles }: CommonComponentProps) => {

    const {canDrop, drop } = useMaterailDrop(['Button', 'Container'], id);

    const divRef = useRef<HTMLDivElement>(null);

    const [_, drag] = useDrag({
        type: name,
        item: {
            type: name,
            dragType: 'move',
            id: id
        }
    });

    useEffect(() => {
        drop(divRef);
        drag(divRef);
    }, []);
    
    return (
        <div 
            data-component-id={id}
            ref={divRef}
            style={styles}
            className={`min-h-[100px] p-[20px] ${ canDrop ? 'border-[2px] border-[blue]' : 'border-[1px] border-[#000]'}`}
        >{children}</div>
    )
}

export default Container;

接下来我们加一下 Table 的物料组件:

materials/Table/dev.tsx

import { Table as AntdTable } from 'antd';
import React, { useEffect, useMemo, useRef } from 'react';
import { CommonComponentProps } from '../../interface';
import { useMaterailDrop } from '../../hooks/useMaterailDrop';
import { useDrag } from 'react-dnd';

function Table({ id, name, children, styles }: CommonComponentProps) {

    const {canDrop, drop } = useMaterailDrop(['TableColumn'], id);
    
    const divRef = useRef<HTMLDivElement>(null);

    const [_, drag] = useDrag({
        type: name,
        item: {
            type: name,
            dragType: 'move',
            id: id
        }
    });

    useEffect(() => {
        drop(divRef);
        drag(divRef);
    }, []);

    const columns = useMemo(() => {
        return React.Children.map(children, (item: any) => {
            return {
                title: <div className='m-[-16px] p-[16px]' data-component-id={item.props?.id}>{item.props?.title}</div>,
                dataIndex: item.props?.dataIndex,
                key: item
            }
        })
    }, [children]);

    return (
        <div
            className={`w-[100%] ${canDrop ? 'border-[2px] border-[blue]' : 'border-[1px] border-[#000]'}`}
            ref={divRef}
            data-component-id={id}
            style={styles}
        >
            <AntdTable
                columns={columns}
                dataSource={[]}
                pagination={false}
            />
        </div>
    );
}

export default Table;

添加 drop、drag 的处理,用 antd 的 table 来渲染。

这里 columns 的处理比较巧妙:

我们拖拽 TableColumn 组件过来的时候,用 React.Children 遍历,把它变为 columns 配置。

当然,这个 TableColumn 组件还没写。

在 componentConfig 添加 Table 组件的配置:

Table: {
    name: 'Table',
    defaultProps: {},
    desc: '表格',
    setter: [
        {
          name: 'url',
          label: 'url',
          type: 'input',
        },
    ],
    dev: TableDev,
    prod: TableDev
}

然后在 Page、Modal、Container 组件里支持下 Table 的 drop:

试一下:

没啥问题。

然后再实现下 TableColumn 组件:

materials/TableColumn/dev.tsx

const TableColumn = () => {
  return <></>
}

export default TableColumn;

materials/TableColumn/prod.tsx

const TableColumn = () => {
    return <></>
}

export default TableColumn;

这只是我们做 column 配置用的,不需要渲染内容。

在 ColumnConfig 加一下配置:

TableColumn: {
    name: 'TableColumn',
    desc: '表格列',
    defaultProps: {
        dataIndex:`col_${new Date().getTime()}`,
        title: '列名'
    },
    setter: [
        {
          name: 'type',
          label: '类型',
          type: 'select',
          options: [
            {
              label: '文本',
              value: 'text',
            },
            {
              label: '日期',
              value: 'date',
            },
          ],
        },
        {
          name: 'title',
          label: '标题',
          type: 'input',
        },
        {
          name: 'dataIndex',
          label: '字段',
          type: 'input',
        },
      ],
    dev: TableColumnDev,
    prod: TableColumnProd,
}

试下效果:

我们用 TableColumn 组件来配置字段。

然后再来实现 Table 组件的 prod 版本:

materials/Table/prod.tsx

import { Table as AntdTable } from 'antd';
import dayjs from 'dayjs';
import React, { useEffect, useMemo, useState } from 'react';
import axios from 'axios';
import { CommonComponentProps } from '../../interface';

const Table = ({ url, children }: CommonComponentProps) => {

  const [data, setData] = useState<Array<Record<string, any>>>([]);
  const [loading, setLoading] = useState(false);

  const getData = async () => {
    if (url) {
      setLoading(true);

      const { data } = await axios.get(url);
      setData(data);

      setLoading(false);
    }
  }

  useEffect(() => {
    getData();
  }, []);

  const columns = useMemo(() => {
    return React.Children.map(children, (item: any) => {
        if (item?.props?.type === 'date') {
            return {
                title: item.props?.title,
                dataIndex: item.props?.dataIndex,
                render: (value: any) => value ? dayjs(value).format('YYYY-MM-DD') : null,
            }
        } else {
            return {
                title: item.props?.title,
                dataIndex: item.props?.dataIndex,
            }
        }
    })
  }, [children]);

  return (
    <AntdTable
      columns={columns}
      dataSource={data}
      pagination={false}
      rowKey="id"
      loading={loading}
    />
  );
}

export default Table;

生产环境的 Table 需要请求 url,拿到数据后设置到 table。

并且渲染列的时候,如果是 date,要用 dayjs 做下格式化。

安装下用到的包:

npm install --save axios
npm install --save dayjs

改下 componentConfig 里的组件:

试一下:

可以看到,确实发请求了。

只不过现在没这个接口。

我们用 nest 创建一个后端服务:

npx @nestjs/cli new lowcode-demo-backend

改下 AppController,加一个接口:

@Get('data')
data() {
    return [
      { name: '光光', sex: '男', birthday: new Date('1994-07-07').getTime() },
      { name: '东东', sex: '男', birthday: new Date('1995-06-06').getTime() },
      { name: '小红', sex: '女', birthday: new Date('1996-08-08').getTime() }
    ]
}

在 main.ts 开启跨域:

把服务跑起来:

npm run start:dev

浏览器访问下:

这样接口就有了。

我们再来试下 Table 组件:

添加三个 TableColumn,配置下字段。

然后在 Table 配置下 url:

再点击预览:

这样,Table 组件就会请求 url,然后根据配置渲染表格

案例代码上传了小册仓库,可以切换到这个 commit 查看:

git reset --hard 3df08cf3e09d69817f1bc75bf1b0f9f5e8cb41c4

总结

这节我们实现了物料组件拖拽改变位置,并实现了 Table 组件。

拖拽改变位置只要在物料组件上加上 useDrag 就可以了,要注意区分 add 和 move 的情况,加上标识,分别做处理。

Table 组件可以配置 url,然后拖拽 TableColumn 进来,TableColumn 可以配置字段信息。

Preview 渲染的时候,根据 url 请求接口,然后根据 columns 的配置来渲染数据。

这样,Table 的物料组件就完成了。

返回上级