• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

在 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 的物料组件就完成了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
76.低代码编辑器:组件联动
Next
78.低代码编辑器:Form组件、store持久化