100202Title和Input组件_编辑器-react-仿低代码平台项目

文章目录

    • [1 开发两个问卷组件](#1 开发两个问卷组件)
      • [1.1 Title组件](#1.1 Title组件)
      • [1.2 Input组件](#1.2 Input组件)
      • [1.3 画布静态展示TItle和Input](#1.3 画布静态展示TItle和Input)
    • [2 Ajax获取问卷数据,并存储到Redux store](#2 Ajax获取问卷数据,并存储到Redux store)
      • [2.1 API接口](#2.1 API接口)
      • [2.2 组件列表存储到Redux store统一管理](#2.2 组件列表存储到Redux store统一管理)
      • [2.3 重构useLoadQuestionData](#2.3 重构useLoadQuestionData)
    • [3 在画布显示问卷列表,点击可选中](#3 在画布显示问卷列表,点击可选中)
      • [3.1 Redux获取组件列表](#3.1 Redux获取组件列表)
      • [3.2 根据Redux组件列表渲染组件](#3.2 根据Redux组件列表渲染组件)
      • [3.3 点击选择组件,共享selectedId](#3.3 点击选择组件,共享selectedId)
    • 关于

1 开发两个问卷组件

1.1 Title组件

数据类型和默认值,interface.ts代码如下:

ts 复制代码
export type QuestionTitlePropsType = {
  text?: string;
  level?: 1 | 2 | 3 | 4 | 5;
  isCenter?: boolean;
};

export const QuestionTitleDefaultProps: QuestionTitlePropsType = {
  text: "一行标题",
  level: 1,
  isCenter: false,
};

组件Component.tsx代码如下所示:

tsx 复制代码
import { FC } from "react";
import { Typography } from "antd";
import { QuestionTitlePropsType, QuestionTitleDefaultProps } from "./interface";

const { Title } = Typography;

const QuestionTitle: FC<QuestionTitlePropsType> = (
  props: QuestionTitlePropsType,
) => {
  const {
    text = "",
    level = 1,
    isCenter = false,
  } = { ...QuestionTitleDefaultProps, ...props };

  const genFontSize = (level: number) => {
    if (level === 1) {
      return "24px";
    } else if (level === 2) {
      return "20px";
    } else if (level === 3) {
      return "16px";
    } else if (level === 4) {
      return "12px";
    } else {
      return "16px";
    }
  };

  return (
    <Title
      level={level}
      style={{
        textAlign: isCenter ? "center" : "start",
        marginBottom: "0",
        fontSize: genFontSize(level),
      }}
    >
      {text}
    </Title>
  );
};

export default QuestionTitle;

1.2 Input组件

数据类型和默认值,interface.ts代码如下:

ts 复制代码
export type QuestionInputPropsType = {
  title?: string;
  placeholder?: string;
};

export const QuestionInputDefaultProps: QuestionInputPropsType = {
  title: "输入框标题",
  placeholder: "请输入...",
};

组件Component.tsx代码如下所示:

tsx 复制代码
import { FC } from "react";
import { Typography, Input } from "antd";
import { QuestionInputPropsType, QuestionInputDefaultProps } from "./interface";

const { Paragraph } = Typography;

const QuestionInput: FC<QuestionInputPropsType> = (
  props: QuestionInputPropsType,
) => {
  const { text = "", placeholder = "" } = {
    ...QuestionInputDefaultProps,
    ...props,
  };

  return (
    <div>
      <Paragraph strong>{text}</Paragraph>
      <div>
        <Input placeholder={placeholder}></Input>
      </div>
    </div>
  );
};

export default QuestionInput;

1.3 画布静态展示TItle和Input

组件EditCanvas.tsx代码如下:

tsx 复制代码
import { FC } from "react";
import styles from "./EditCanvas.module.scss";

import { Spin } from "antd";

import QuestionTitle from "@/components/QuestionComponents/QuestionTitle/Component";
import QuestionInput from "@/components/QuestionComponents/QuestionInput/Component";

type PropsType = {
  loading: boolean;
};

const EditCanvas: FC<PropsType> = ({ loading }) => {
  if (loading) {
    return (
      <div style={{ textAlign: "center", marginTop: "24px" }}>
        <Spin />
      </div>
    );
  }
  return (
    <div className={styles.canvas}>
      <div className={styles["component-wrapper"]}>
        <div className={styles.component}>
          <QuestionTitle />
        </div>
      </div>
      <div className={styles["component-wrapper"]}>
        <div className={styles.component}>
          <QuestionInput />
        </div>
      </div>
    </div>
  );
};

export default EditCanvas;

样式EditCanvas.module.scss代码如下:

scss 复制代码
.canvas {
  min-height: 100%;
  background-color: #fff;
  overflow: hidden;
}

.component-wrapper {
  margin: 12px;
  border: 1px solid #fff;
  padding: 12px;
  border-radius: 3px;

  &:hover {
    border-color: #d9d9d9;
  }
}

.component {
  pointer-events: none; // 屏蔽鼠标行为,组件不让被点击到
}

效果如下图所示:

2 Ajax获取问卷数据,并存储到Redux store

2.1 API接口

获取问卷详情API接口如下:

js 复制代码
{
    // 获取问卷信息
    url: '/api/question/:id',
    method: 'get',
    response() {
      return {
        errno: 0,
        data: {
          id: Random.id(),
          title: Random.ctitle(),
          componentList: [
            {
              fe_id: Random.id(),
              type: 'questionTitle',
              title: '标题',
              props: {
                text: '个人信息调研',
                level: 1,
                isCenter: false
              }
            },{
              fe_id: Random.id(),
              type: 'questionInput',
              title: '输入框',
              props: {
                text: '你的姓名',
                placeholder: '请输入姓名...',
              }
            },{
              fe_id: Random.id(),
              type: 'questionInput',
              title: '输入框',
              props: {
                text: '你的电话',
                placeholder: '请输入电话...',
              }
            },
          ]
        },
      }
    }
  }

2.2 组件列表存储到Redux store统一管理

src/store/componentsReducer/index.ts代码如下:

ts 复制代码
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

import { ComponentPropsType } from "@/components/QuestionComponents";

export type ComponentInfoType = {
  fe_id: string;
  type: string;
  title: string;
  props: ComponentPropsType;
};

export type ComponentsStateType = {
  componentList: Array<ComponentInfoType>;
};

const INIT_STATE: ComponentsStateType = {
  componentList: [],
  // 其他扩展
};

export const componentsSlice = createSlice({
  name: "components",
  initialState: INIT_STATE,
  reducers: {
    // 重置所有组件
    resetComponents(
      state: ComponentsStateType,
      action: PayloadAction<ComponentsStateType>,
    ) {
      return action.payload;
    },
  },
});

export const { resetComponents } = componentsSlice.actions;
export default componentsSlice.reducer;

组件属性类型src/components/QuestionComponents/index.ts代码如下:

ts 复制代码
import { FC } from "react";

import QuestionInputConf, { QuestionInputPropsType } from "./QuestionInput";
import QuestionTitleConf, { QuestionTitlePropsType } from "./QuestionTitle";

// 各个组件属性类型
export type ComponentPropsType = QuestionInputPropsType &
  QuestionTitlePropsType;

// 统一组件配置
export type ComponentConfType = {
  title: string;
  type: string;
  Component: FC<ComponentPropsType>;
  defaultProps: ComponentPropsType;
};

// 全部组件配置列表
const componentConfList: ComponentConfType[] = [
  QuestionInputConf,
  QuestionTitleConf,
];

// 根据组件类型获取组件
export function getComponentConfByType(type: string) {
  return componentConfList.find((c) => c.type === type);
}

2.3 重构useLoadQuestionData

代码如下所示:

ts 复制代码
import { useParams } from "react-router-dom";
import { useRequest } from "ahooks";
import { getQuestionApi } from "@/api/question";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { resetComponents } from "@/store/componentsReducer";

/**
 * 获取带加载状态的问卷信息
 * @returns loading状态,问卷信息
 */
function useLoadQuestionData() {
  const { id = "" } = useParams();
  const dispatch = useDispatch();
  // ajax 加载
  const { data, loading, error, run } = useRequest(
    async (id: string) => {
      if (!id) {
        throw new Error("没有问卷 id");
      }
      const data = await getQuestionApi(id);
      return data;
    },
    {
      manual: true,
    },
  );

  // 根据获取的data,设置store
  useEffect(() => {
    if (!data) {
      return;
    }
    const { componentList = [] } = data;
    // componentList 存入redux store
    dispatch(resetComponents({ componentList }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data]);
  // 根据id变化,加载问卷数据
  useEffect(() => {
    run(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [id]);

  return { loading, error };
}

export default useLoadQuestionData;

3 在画布显示问卷列表,点击可选中

3.1 Redux获取组件列表

自定义hook-useGetComponentInfo.ts 代码如下所示:

ts 复制代码
import { StateType } from "@/store";
import { useSelector } from "react-redux";
import { ComponentsStateType } from "@/store/componentsReducer";

function useGetComponentInfo() {
  const components = useSelector<StateType>(
    (state) => state.components,
  ) as ComponentsStateType;
  const { componentList = [] } = components;
  return { componentList };
}

export default useGetComponentInfo;

3.2 根据Redux组件列表渲染组件

EditCanvas.tsx代码如下所示:

tsx 复制代码
import { FC } from "react";
import styles from "./EditCanvas.module.scss";

import { Spin } from "antd";

// import QuestionTitle from "@/components/QuestionComponents/QuestionTitle/Component";
// import QuestionInput from "@/components/QuestionComponents/QuestionInput/Component";
import useGetComponentInfo from "@/hooks/useGetComponentInfo";
import { getComponentConfByType } from "@/components/QuestionComponents";
import { ComponentInfoType } from "@/store/componentsReducer";

type PropsType = {
  loading: boolean;
};

function genComponent(componentInfo: ComponentInfoType) {
  const { type, props } = componentInfo;
  const componentConf = getComponentConfByType(type);
  if (componentConf == null) {
    return null;
  }
  const { Component } = componentConf;
  return <Component {...props} />;
}

const EditCanvas: FC<PropsType> = ({ loading }) => {
  const { componentList = [] } = useGetComponentInfo();

  if (loading) {
    return (
      <div style={{ textAlign: "center", marginTop: "24px" }}>
        <Spin />
      </div>
    );
  }
  return (
    <div className={styles.canvas}>
      {componentList.map((c) => {
        const { fe_id } = c;
        return (
          <div key={fe_id} className={styles["component-wrapper"]}>
            <div className={styles.component}>{genComponent(c)}</div>
          </div>
        );
      })}
      {/* <div className={styles["component-wrapper"]}>
        <div className={styles.component}>
          <QuestionTitle />
        </div>
      </div>
      <div className={styles["component-wrapper"]}>
        <div className={styles.component}>
          <QuestionInput />
        </div>
      </div> */}
    </div>
  );
};

export default EditCanvas;

3.3 点击选择组件,共享selectedId

componentsReducer/index.ts 添加selectId及修改,代码如下:

ts 复制代码
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { produce } from "immer";
import { ComponentPropsType } from "@/components/QuestionComponents";

export type ComponentInfoType = {
  fe_id: string;
  type: string;
  title: string;
  props: ComponentPropsType;
};

export type ComponentsStateType = {
  selectedId: string;
  componentList: Array<ComponentInfoType>;
};

const INIT_STATE: ComponentsStateType = {
  selectedId: "",
  componentList: [],
  // 其他扩展
};

export const componentsSlice = createSlice({
  name: "components",
  initialState: INIT_STATE,
  reducers: {
    // 重置所有组件
    resetComponents(
      state: ComponentsStateType,
      action: PayloadAction<ComponentsStateType>,
    ) {
      return action.payload;
    },
    // 切换选中组件
    changeSelectedId: produce((draft: ComponentsStateType, action: PayloadAction<string>) => {
      draft.selectedId = action.payload;
    }),
  },
});

export const { resetComponents, changeSelectedId } = componentsSlice.actions;
export default componentsSlice.reducer;

新增选中 css 样式,EditCanvas.module 新增代码如下所示:

scss 复制代码
.selected {
  border-color: #1890ff !important;
}

组件点击选择,点击画布空白处取消,阻止默认冒泡行为,EditCanvas.tsx代码如下:

tsx 复制代码
import { FC } from "react";
import styles from "./EditCanvas.module.scss";

import { Spin } from "antd";

// import QuestionTitle from "@/components/QuestionComponents/QuestionTitle/Component";
// import QuestionInput from "@/components/QuestionComponents/QuestionInput/Component";
import useGetComponentInfo from "@/hooks/useGetComponentInfo";
import { getComponentConfByType } from "@/components/QuestionComponents";
import { ComponentInfoType } from "@/store/componentsReducer";
import { useDispatch } from "react-redux";
import classNames from "classnames";
import { changeSelectedId } from "@/store/componentsReducer";

type PropsType = {
  loading: boolean;
};

function genComponent(componentInfo: ComponentInfoType) {
  const { type, props } = componentInfo;
  const componentConf = getComponentConfByType(type);
  if (componentConf == null) {
    return null;
  }
  const { Component } = componentConf;
  return <Component {...props} />;
}

const EditCanvas: FC<PropsType> = ({ loading }) => {
  const { componentList = [], selectedId } = useGetComponentInfo();
  const dispatch = useDispatch();

  // 获取当前选中的组件
  function handleClick(e: React.MouseEvent<HTMLDivElement>, fe_id: string) {
    e.stopPropagation();
    dispatch(changeSelectedId(fe_id));
  }

  if (loading) {
    return (
      <div style={{ textAlign: "center", marginTop: "24px" }}>
        <Spin />
      </div>
    );
  }
  return (
    <div className={styles.canvas}>
      {componentList.map((c) => {
        const { fe_id } = c;

        // 拼接 class name
        const wrapperDefaultClassName = styles["component-wrapper"];
        const selectedClassName = styles.selected;
        const wrapperClassName = classNames({
          [wrapperDefaultClassName]: true,
          [selectedClassName]: fe_id === selectedId,
        });

        return (
          <div key={fe_id} className={wrapperClassName} onClick={(e) => handleClick(e, fe_id)}>
            <div className={styles.component}>{genComponent(c)}</div>
          </div>
        );
      })}
      {/* <div className={styles["component-wrapper"]}>
        <div className={styles.component}>
          <QuestionTitle />
        </div>
      </div>
      <div className={styles["component-wrapper"]}>
        <div className={styles.component}>
          <QuestionInput />
        </div>
      </div> */}
    </div>
  );
};

export default EditCanvas;

默认selectId为组件列表第一个,没有不选中,useLoadQuestionData.ts代码如下所示:

ts 复制代码
import { useParams } from "react-router-dom";
import { useRequest } from "ahooks";
import { getQuestionApi } from "@/api/question";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { resetComponents } from "@/store/componentsReducer";

/**
 * 获取带加载状态的问卷信息
 * @returns loading状态,问卷信息
 */
function useLoadQuestionData() {
  const { id = "" } = useParams();
  const dispatch = useDispatch();
  // ajax 加载
  const { data, loading, error, run } = useRequest(
    async (id: string) => {
      if (!id) {
        throw new Error("没有问卷 id");
      }
      const data = await getQuestionApi(id);
      return data;
    },
    {
      manual: true,
    },
  );

  // 根据获取的data,设置store
  useEffect(() => {
    if (!data) {
      return;
    }
    const { componentList = [] } = data;

    // 获取默认的 selectedId
    let selectedId = "";
    if (componentList.length > 0) {
      selectedId = componentList[0].fe_id;
    }

    // componentList 存入redux store
    dispatch(resetComponents({ componentList, selectedId }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data]);
  // 根据id变化,加载问卷数据
  useEffect(() => {
    run(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [id]);

  return { loading, error };
}

export default useLoadQuestionData;

效果如下图所示:


关于

❓QQ:806797785

⭐️仓库地址:https://gitee.com/gaogzhen

⭐️仓库地址:https://github.com/gaogzhen

1\][react官网](https://reactjs.org/)\[CP/OL\]. \[2\][Redux官网](https://redux.js.org/)\[CP/OL\].

相关推荐
吃奥特曼的饼干41 分钟前
React useEffect 清理函数:别让依赖数组坑了你!
前端·react.js
随笔记1 小时前
react中函数式组件和类组件有什么区别?新建的react项目用函数式组件还是类组件?
前端·react.js·typescript
emojiwoo2 小时前
React 状态管理:useState 与 useDatePersistentState 深度对比
前端·javascript·react.js
D11_2 小时前
【React】JSX基础
前端·react.js·前端框架
晴空雨2 小时前
Zustand vs Redux Toolkit:现代 React 状态管理深度对比
前端·react.js
梨子同志2 小时前
React 组件
react.js
XiaoMu_0013 小时前
【Vue vs React:前端框架深度对比分析】
vue.js·react.js·前端框架
GISer_Jing7 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
一瞬祈望15 小时前
Visual Studio Code 基础设置指南
vscode·编辑器