后台更新的内容在网站上不显示,wordpress主题结构,网站开发测量像素工具,wordpress 导出word问卷编辑器 
Date: February 20, 2025 4:17 PM (GMT8) 目标 
完成问卷编辑器的设计和开发完成复杂系统的 UI 组件拆分完成复杂系统的数据结构设计 内容 
需求分析技术方案设计开发 
注意事项#xff1a; 
需求指导设计#xff0c;设计指导开发。前两步很重要页面复杂的话8) 目标 
完成问卷编辑器的设计和开发完成复杂系统的 UI 组件拆分完成复杂系统的数据结构设计 内容 
需求分析技术方案设计开发 
注意事项 
需求指导设计设计指导开发。前两步很重要页面复杂的话可以设计边开发 画布 UI 分析 
画布UI组成 画布基础构建 
画布Y轴滚动 
要点 
flex 布局居中对齐画布 Y 向滚动 
效果 question/Edit/index.tsx 
import React, { FC } from react
import styles from ./index.module.scss
// import { useParams } from react-router-dom
// import useLoadQuestionData from ../../../hooks/useLoadQuestionDataconst Edit: FC  ()  {// const { id   }  useParams()// const { loading, data }  useLoadQuestionData()return (div className{styles.container}div style{{ backgroundColor: #fff, height: 40px }}Header/divdiv className{styles[content-wrapper]}div className{styles.content}div className{styles.left}Left/divdiv className{styles.main}div className{styles[canvas-wrapper]}div style{{ height: 900px }}画布滚动测试/div/div/divdiv className{styles.right}Right/div/div/div/div)
}export default Edit 
question/Edit/index.module.scss 
.container {display: flex;flex-direction: column;height: 100vh;background-color: #f0f2f5;
}.content-wrapper {flex: auto;padding: 12px 0;
}.content {display: flex;margin: 0 24px;height: 100%;.left {width: 285px;background-color: #fff;padding: 0 12px;}.main {flex: 1;position: relative;overflow: hidden;.canvas-wrapper {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);width: 400px;height: 712px;background-color: #fff; overflow: auto;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);}}.right {width: 300px;background-color: #fff;padding: 0 12px;}
} 开发问卷组件Title 和 Input 
开发组件 Title 
目标 要点 
组件默认参数设置{ ...QuestionInputDefaultProps, ...props } 
Component.ts 
import React, { FC } from react
import { Typography, Input } from antd
import { QuestionTitleProps, QuestionInputDefaultProps } from ./interfaceconst { Paragraph }  Typography
const QuestionTitle: FCQuestionTitleProps  (props: QuestionTitleProps)  {const { title  , placeholder   }  {...QuestionInputDefaultProps,...props,}return (divParagraph strong{title}/ParagraphdivInput placeholder{placeholder}/Input/div/div)
}
export default QuestionTitle 
interface.ts 
export type QuestionTitleProps  {title?: stringplaceholder?: string
}export const QuestionInputDefaultProps: QuestionTitleProps  {title: 输入框标题,placeholder: 请输入内容,
} 开发组件 Input 
目标 要点 
Input标题样式函数设计 genFontSize 
Component.ts 
import React, { FC } from react
import { Typography } from antd
import { QuestionTitleProps, defaultQuestionTitleProps } from ./interfaceconst { Title }  Typography
const QuestionTitle: FCQuestionTitleProps  (props: QuestionTitleProps)  {const {text  ,level  1,isCenter  false,}  { ...defaultQuestionTitleProps, ...props }const genFontSize  (level: number)  {if (level  1) return 24pxif (level  2) return 20pxif (level  3) return 16pxif (level  4) return 14pxif (level  5) return 12pxreturn 24px}return (Titlelevel{level}style{{textAlign: isCenter ? center : left,marginBottom: 0px,fontSize: genFontSize(level),}}{text}/Title)
}export default QuestionTitle 
interface.ts 
export type QuestionTitleProps  {text?: stringlevel?: 1 | 2 | 3 | 4 | 5isCenter?: boolean
}export const defaultQuestionTitleProps: QuestionTitleProps  {text: 一行标题,level: 1,isCenter: false,
}画布集成组件测试 
目标 要点 
画布元素禁止点击样式设计 .componet标题样式上方多余空距问题处理见下方 
EditCanvas.tsx 
import React, { FC } from react
import styles from ./EditCanvas.module.scss
import QuestionTitle from ../../../component/QuestionComponents/QuestionTitle/Component
import QuestionInput from ../../../component/QuestionComponents/QuestionInput/Componentconst EditCanvas: FC  ()  {return (div className{styles.canvas}div className{styles[component-wrapper]}div className{styles.component}QuestionTitle //div/divdiv className{styles[component-wrapper]}div className{styles.component}QuestionInput //div/div/div)
}export default EditCanvasEidtCanvas.module.scss 
.canvas {min-height: 100%;background-color: #fff;overflow: hidden;
}.component-wrapper {margin: 12px;border: 1px solid #fff;padding: 12px;border-radius: 3px;// 新增修复代码推荐方案:global(.ant-typography) {margin-block-start: 0 !important;margin-block-end: 0 !important;}:hover {border: 1px solid #d9d9d9;}
}.componet {pointer-events: none; // 禁止点击
}问卷数据获取与存储 
问卷信息存储在 Redux 中的原因 
组件间需要不断联动如下所示段落的选中以及修改都涉及到相同的数据的访问。因此建议把问卷信息存储在 Redux 中便于组件间共享使用。 组件数据结构设计 
服务端mock数据 {url: /api/question/:id,method: get,response() {return {errno: 0,data: {id: Random.id(),title: Random.ctitle(),componentList: [{id: Random.id(),type: questionTitle,  // 组件类型不能重复前后端统一好title: 这是一个文本组件,props: {text: 文本内容,level1,isCenter: false}},{id: Random.id(),type: questionInput,title: 这是一个输入框组件,props: {title: 你的名字,placeholder: 请输入内容}},{id: Random.id(),type: questionInput,title: 这是一个输入框组件,props: {title: 你的电话,placeholder: 请输入内容}}],}}}}, Ajax 加载数据 
要点 
Hook useLoadQuestionData 设计 问卷信息获取函数id 变化更新问卷信息数据更新后存储在 Redux 中  
useLoadQuestionData.ts 
import { useEffect } from react
import { useParams } from react-router-dom
import { useDispatch } from react-redux
import { getQuestionService } from ../services/question
import { useRequest } from ahooks
import { resetComponentList } from ../store/componentReducerfunction useLoadQuestionData() {const { id   }  useParams()const dispatch  useDispatch()// 问卷信息获取函数const { data, loading, error, run }  useRequest(async (id: string)  {const data  await getQuestionService(id)return data},{manual: true,})// 数据更新后存储在 Redux 中useEffect(()  {if (!data) returnconst { title  , componentList }  dataif (!componentList || componentList.length  0) returnconst action  resetComponentList({ componentList })dispatch(action)}, [data])// id 变化更新问卷信息useEffect(()  {run(id)}, [id])return {loading,error,}
}export default useLoadQuestionData Redux 数据存储 
要点 
设计 componentReducer定义类型以及切片设计 index作为各个切片 类型 和 reducer 的统一收口 
文件树 
│   ├── store
│   │   ├── componentReducer
│   │   │   └── index.ts
│   │   ├── index.ts
│   │   └── userReducer.tscomponentReducer/index.ts 
import { createSlice, PayloadAction } from reduxjs/toolkit
import { ComponentPropsType } from ../../component/QuestionComponentsexport type ComponentInfoType  {fe_id: stringtype: stringtitle: stringprops: ComponentPropsType
}export type ComponentsStateType  {componentList: ArrayComponentInfoType
}const INIT_STATE: ComponentsStateType  {componentList: [],// 其他拓展
}export const componentsSlice  createSlice({name: component,initialState: INIT_STATE,reducers: {resetComponentList: (state: ComponentsStateType,action: PayloadActionComponentsStateType)  {return action.payload},},
})export const { resetComponentList }  componentsSlice.actions
export default componentsSlice.reducer 
index.ts 
import { configureStore } from reduxjs/toolkit
import userReducer, { UserStateType } from ./userReducer
import componentReducer, { ComponentsStateType } from ./componentReducerexport type StateType  {user: UserStateTypecomponents: ComponentsStateType
}export default configureStore({reducer: {user: userReducer,components: componentReducer,// 组件列表// 问卷信息},
})画布显示问卷列表 
组件类型设定 
要点 
整合各组件 prop type整合各组件 配置列表 
文件树 
│   │   ├── QuestionComponents
│   │   │   ├── QuestionInput
│   │   │   │   ├── Component.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── interface.ts
│   │   │   ├── QuestionTitle
│   │   │   │   ├── Component.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── interface.ts
│   │   │   └── index.tsQuestionComponents/index.ts 
import QuestionInputConf, { QuestionInputPropsType } from ./QuestionInput
import QuestionTitleConf, { QuestionTitlePropsType } from ./QuestionTitle// 各个组件的 prop type
export type ComponentPropsType  QuestionInputPropsType  QuestionTitlePropsType// 组件的配置
export type ComponentConfType  {title: stringtype: stringComponent: React.FCComponentPropsTypedefaultProps: ComponentPropsType
}// 全部组件配置的列表
const componentConfList: ComponentConfType[]  [QuestionInputConf,QuestionTitleConf,
]export function getComponentConfByType(type: string) {return componentConfList.find(c  c.type  type)
}画布动态显示组件列表 
效果 要点 
根据组件类型动态渲染指定组件 返回组件函数实现getComponent 用于根据组件类型返回指定组件  
question/Edit/EditCanvas.tsx 
import React, { FC } from react
import styles from ./EditCanvas.module.scss
// import QuestionTitle from ../../../component/QuestionComponents/QuestionTitle/Component
// import QuestionInput from ../../../component/QuestionComponents/QuestionInput/Component
import { Spin } from antd
import useGetComponentInfo from ../../../hooks/useGetComponentInfo
import { getComponentConfByType } from ../../../component/QuestionComponents
import { ComponentInfoType } from ../../../store/componentReducertype PropsType  {loading: boolean
}function getComponent(componentInfo: ComponentInfoType) {const { type, props }  componentInfoconst componentConf  getComponentConfByType(type)if (!componentConf) {return null}const { Component }  componentConfreturn Component {...props} /
}const EditCanvas: FCPropsType  ({ loading })  {const { componentList }  useGetComponentInfo()if (loading) {return (div style{{ textAlign: center, padding: 20px }}Spin //div)}return (div className{styles.canvas}{componentList.map(c  {const { fe_id }  creturn (div key{fe_id} className{styles[component-wrapper]}div className{styles.component}{getComponent(c)}/div/div)})}/div)// 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 点击画布选中组件 
效果 要点 
选中画布中组件显示 点击画布组件选中点击空白不选中。clearSelectedId() 和 handleClick() 实现 冒泡机制实现 组件 selectedId 与 Redux 绑定 ComponentsStateType 设定 selectedIduseLoadQuestionData 设定进入画布时默认选中组件  处理 Immer 中 draft 为空的问题 
EditCanvas.tsx 
import React, { FC, MouseEvent } from react
import styles from ./EditCanvas.module.scss
import { useDispatch } from react-redux
import useGetComponentInfo from ../../../hooks/useGetComponentInfo
import { getComponentConfByType } from ../../../component/QuestionComponents
import classNames from classnames
import {ComponentInfoType,changeSelectedId,
} from ../../../store/componentReducertype PropsType  {loading: boolean
}function getComponent(componentInfo: ComponentInfoType) {const { type, props }  componentInfoconst componentConf  getComponentConfByType(type)if (!componentConf) {return null}const { Component }  componentConfreturn Component {...props} /
}const EditCanvas: FCPropsType  ({ loading })  {const { componentList, selectedId }  useGetComponentInfo()const dispatch  useDispatch()// 点击冒泡机制实现function handleClick(event: MouseEvent, id: string) {event.stopPropagation()dispatch(changeSelectedId(id))}if (loading) {return (div style{{ textAlign: center, padding: 20px }}Spin //div)}return (div className{styles.canvas}{componentList.map(c  {const { fe_id }  c// 拼接 class nameconst wrapperDefaultClassName  styles[component-wrapper]const selectedClassName  styles.selectedconst wrapperClassName  classNames({[wrapperDefaultClassName]: true,[selectedClassName]: fe_id  selectedId,})return (divkey{fe_id}className{wrapperClassName}onClick{e  handleClick(e, fe_id || )}div className{styles.component}{getComponent(c)}/div/div)})}/div)
}export default EditCanvas 
/Edit/index.tsx 
import React, { FC } from react
import styles from ./index.module.scss
import EditCanvas from ./EditCanvas
import { changeSelectedId } from ../../../store/componentReducer
import { useDispatch } from react-redux
import useLoadQuestionData from ../../../hooks/useLoadQuestionDataconst Edit: FC  ()  {const { loading }  useLoadQuestionData()const dispatch  useDispatch()function clearSelectedId() {dispatch(changeSelectedId())}return (div className{styles.container}div style{{ backgroundColor: #fff, height: 40px }}Header/divdiv className{styles[content-wrapper]}div className{styles.content}div className{styles.left}Left/divdiv className{styles.main} onClick{clearSelectedId}div className{styles[canvas-wrapper]}div style{{ height: 900px }}EditCanvas loading{loading} //div/div/divdiv className{styles.right}Right/div/div/div/div)
}export default Edit 
store/componentReducer/index.ts 
import { createSlice, PayloadAction } from reduxjs/toolkit
import { ComponentPropsType } from ../../component/QuestionComponentsexport type ComponentInfoType  {fe_id: stringtype: stringtitle: stringprops: ComponentPropsType
}// ComponentsStateType 设定 selectedId
export type ComponentsStateType  {selectedId: stringcomponentList: ArrayComponentInfoType
}const INIT_STATE: ComponentsStateType  {selectedId: ,componentList: [],// 其他拓展
}export const componentsSlice  createSlice({name: component,initialState: INIT_STATE,reducers: {resetComponentList: (state: ComponentsStateType,action: PayloadActionComponentsStateType)  {return action.payload},changeSelectedId: (draft: ComponentsStateType,action: PayloadActionstring)  {draft.selectedId  action.payload || },},
})export const { resetComponentList, changeSelectedId }  componentsSlice.actions
export default componentsSlice.reducer 
useLoadQuestionData.ts 
useEffect(()  {if (!data) returnconst { componentList }  data// 获取默认的 idlet selectedId  if (componentList.length  0) {const { fe_id }  componentList[0]selectedId  fe_id}if (!componentList || componentList.length  0) returnconst action  resetComponentList({ componentList, selectedId })dispatch(action)
}, [data])fiximmer draft 为空 
**问题**draft打印出来为null请问是什么原因 
// ... 其他代码保持不变 ...export const componentsSlice  createSlice({name: component,initialState: INIT_STATE,reducers: {// 错误写法多层嵌套 producechangeSelectedId: produce( // 需移除外层 produce(draft: ComponentsStateType, action: PayloadActionstring)  {console.log(payload, action.payload)console.log(draft) // 此时 draft 为 nulldraft.selectedId  action.payload || }),},
})原因 
Redux Toolkit 已内置 Immer 集成无需额外使用 produce 包装双重 Immer 包装会导致状态代理失效此时 draft 参数无法正确接收 Redux 状态树直接通过 state 参数操作即可实现安全的不可变更新 
方案 
export const componentsSlice  createSlice({name: component,initialState: INIT_STATE,reducers: {// 正确写法直接使用 ImmerRedux Toolkit 已内置changeSelectedId: (state: ComponentsStateType, action: PayloadActionstring)  {console.log(payload, action.payload)state.selectedId  action.payload || },// ...其他 reducer 保持不变...},
})注意点 
RTK 从 1.0 版本开始内置 Immer。Immer 默认用于 createSlice 和 createReducer允许开发者以“可变”的方式更新状态。如果需要可以通过配置禁用 Immer但绝大多数情况下默认启用 Immer 是推荐的做法。 组件库面板 
组件分组显示 
需求 要点 
组件库配置组件库组件列表渲染显示组件库组件点击框图、鼠标样式设计 
思路 
先对组件库进行配置然后在页面中对应位置进行渲染组件即可。 
QuestionComponents/index.ts 
import QuestionInputConf, { QuestionInputPropsType } from ./QuestionInput
import QuestionTitleConf, { QuestionTitlePropsType } from ./QuestionTitleexport type ComponentPropsType  QuestionInputPropsType  QuestionTitlePropsTypeexport type ComponentConfType  {title: stringtype: stringComponent: React.FCComponentPropsTypedefaultProps: ComponentPropsType
}const componentConfList: ComponentConfType[]  [QuestionInputConf,QuestionTitleConf,
]// 组件库配置
export const componentConfGroup  [{groupId: textGroup,groupName: 文本显示,components: [QuestionTitleConf],},{groupId: inputGroup,groupName: 用户输入,components: [QuestionInputConf],},
]export function getComponentConfByType(type: string) {return componentConfList.find(c  c.type  type)
} 
ComponentLib.tsx 
import React, { FC } from react
import { componentConfGroup } from ../../../component/QuestionComponents
import { Typography } from antd
import { ComponentConfType } from ../../../component/QuestionComponents
import styles from ./ComponentLib.module.scssconst { Title }  Typographyfunction genComponent(c: ComponentConfType) {const { Component }  creturn (div className{styles.wrapper}div className{styles.component}Component //div/div)
}const Lib: FC  ()  {return ({componentConfGroup.map((item, index)  {const { groupId, groupName }  itemreturn (div key{groupId}Titlelevel{3}style{{ fontSize: 16px, marginTop: index  0 ? 20px : 0 }}{groupName}/Titlediv{item.components.map(c  genComponent(c))}/div/div)})}/)
}
export default Lib 
ComponentLib.scss 
.wrapper {padding: 12px;margin-bottom: 12px;cursor: cursor;border: 1px solid #fff;border-radius: 3px;background-color: #fff;:hover {border-color: #d9d9d9;}
}.component {pointer-events: none; // 屏蔽鼠标
} No newline at end of file 组件库添加到画布 
需求 要点 
组件与画布交互逻辑 组件默认插入画布末尾画布中组件选定后组件插入会在其之后 交互逻辑数据实现 ComponentLib 组件插入画布位置逻辑实现addComponent 通过 selected 判断组件插入画布位置  
ComponentLib.tsx 
import React, { FC } from react
import { componentConfGroup } from ../../../component/QuestionComponents
import { Typography } from antd
import { ComponentConfType } from ../../../component/QuestionComponents
import styles from ./ComponentLib.module.scss
import { useDispatch } from react-redux
import { addComponent } from ../../../store/componentReducer
import { nanoid } from reduxjs/toolkitconst { Title }  Typographyconst Lib: FC  ()  {const dispatch  useDispatch()function genComponent(c: ComponentConfType) {const { type, Component }  cfunction handleClick(c: ComponentConfType) {const { title, type, defaultProps }  cdispatch(addComponent({fe_id: nanoid(),type,title,props: defaultProps,}))}return (div key{type} className{styles.wrapper} onClick{()  handleClick(c)}div className{styles.component}Component //div/div)}return ({componentConfGroup.map((item, index)  {const { groupId, groupName }  itemreturn (div key{groupId}Titlelevel{3}style{{ fontSize: 16px, marginTop: index  0 ? 20px : 0 }}{groupName}/Titlediv{item.components.map(c  genComponent(c))}/div/div)})}/)
}
export default Lib 
componentReducer/index.ts 
import { createSlice, PayloadAction } from reduxjs/toolkit
import { ComponentPropsType } from ../../component/QuestionComponentsexport type ComponentInfoType  {fe_id: stringtype: stringtitle: stringprops: ComponentPropsType
}export type ComponentsStateType  {selectedId: stringcomponentList: ArrayComponentInfoType
}const INIT_STATE: ComponentsStateType  {selectedId: ,componentList: [],// 其他拓展
}export const componentsSlice  createSlice({name: component,initialState: INIT_STATE,reducers: {......addComponent: (draft: ComponentsStateType,action: PayloadActionComponentInfoType)  {const newCompontent  action.payloadconst { selectedId, componentList }  draftconst index  componentList.findIndex(c  c.fe_id  selectedId)if (index  0) {draft.componentList.push(newCompontent)} else {draft.componentList.splice(index  1, 0, newCompontent)}draft.selectedId  newCompontent.fe_id},},
})export const { resetComponentList, changeSelectedId, addComponent } componentsSlice.actions
export default componentsSlice.reducer 注意fe_id 和 _id 区别 
要点 
_id是服务端的数据_是因为 mongodb 会为每条数据生成id这是不重复的由 _id 表示fe_id 是前端用于区分组件是否被选中的标记用于组件库与画布的交互 
QuestionCart.tsx 
type PropsType  {_id: stringtitle: stringisPublished: booleanisStar: booleananswerCount: numbercreatedAt: string
}组件属性面板 
点击组件显示属性 
需求 要点 
构建属性面板构造组件属性模块 PropComponet 用于配制组件属性 
│   │   ├── QuestionComponents
│   │   │   ├── QuestionInput
│   │   │   │   ├── Component.tsx
│   │   │   │   ├── PropComponent.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── interface.ts
│   │   │   ├── QuestionTitle
│   │   │   │   ├── Component.tsx
│   │   │   │   ├── PropComponent.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── interface.ts
│   │   │   └──m index.ts思路 
当我们点击画布中的组件后更新组件 selectedId属性面板通过 useGetComponentInfo 获取 selectedComponent  从selectedComponent中层层解构出组件参数props和参数组件 PropComponent  返回 PropComponent {...props} / 传参后的组件即可。   构建属性面板 
RightPanel.tsx 
import React, { FC } from react
import { Tabs } from antd
import { FileTextOutlined, SettingOutlined } from ant-design/icons
import ComponentProp from ./componentPropconst RightPanel: FC  ()  {const tabsItems  [{key: prop,label: (spanFileTextOutlined /属性/span),children: ComponentProp /,},{key: setting,label: (spanSettingOutlined /页面设置/span),children: div页面设置/div,},]return Tabs defaultActiveKeyprop items{tabsItems} /
}export default RightPanel 构造组件属性模块 PropComponet 用于配制组件属性 
question/Edit/ComponentProp.tsx 
import React, { FC } from react
import useGetComponentInfo from ../../../hooks/useGetComponentInfo
import { getComponentConfByType } from ../../../component/QuestionComponentsconst NoProp  ()  {return div style{{ textAlign: center }}请先选择组件/div
}const ComponentProp: FC  ()  {const { selectedComponent }  useGetComponentInfo()if (!selectedComponent) return NoProp /const { type, props }  selectedComponentconst componentConf  getComponentConfByType(type)if (!componentConf) return NoProp /const { PropComponent }  componentConfreturn PropComponent {...props} /
}
export default ComponentPropQuestionComponents/index.ts 新增 PropComponent 
import QuestionInputConf, { QuestionInputPropsType } from ./QuestionInput
import QuestionTitleConf, { QuestionTitlePropsType } from ./QuestionTitle
import { FC } from reactexport type ComponentPropsType  QuestionInputPropsType  QuestionTitlePropsType// 组件的配置: 新增 PropComponent
export type ComponentConfType  {title: stringtype: stringComponent: FCComponentPropsTypePropComponent: FCComponentPropsType // HeredefaultProps: ComponentPropsType
}const componentConfList: ComponentConfType[]  [QuestionInputConf,QuestionTitleConf,
]export const componentConfGroup  [{groupId: textGroup,groupName: 文本显示,components: [QuestionTitleConf],},{groupId: inputGroup,groupName: 用户输入,components: [QuestionInputConf],},
]export function getComponentConfByType(type: string) {return componentConfList.find(c  c.type  type)
} 
QuestionInput/PropComponent.tsx 
import React, { FC } from react
import { useEffect } from react
import { Form, Input } from antd
import { QuestionInputPropsType } from ./interfaceconst PropComponent: FCQuestionInputPropsType  (props: QuestionInputPropsType
)  {const { title, placeholder }  propsconst [form]  Form.useForm()useEffect(()  {form.setFieldsValue({ title, placeholder })}, [title, placeholder])return (Form layoutvertical initialValues{{ title, placeholder }} form{form}Form.Itemlabel标题nametitlerules{[{ required: true, message: 请输入标题 }]}Input //Form.ItemForm.Item labelPlaceholder nameplaceholderInput //Form.Item/Form)
}export default PropComponentQuestionTitle/PropComponent.tsx 
import React, { FC } from react
import { useEffect } from react
import { Form, Input, Select, Checkbox } from antd
import { QuestionTitlePropsType } from ./interfaceconst PropComponent: FCQuestionTitlePropsType  (props: QuestionTitlePropsType
)  {const { text, level, isCenter }  propsconst [form]  Form.useForm()useEffect(()  {form.setFieldsValue({ text, level, isCenter })}, [text, level, isCenter])return (FormlayoutverticalinitialValues{{ text, level, isCenter }}form{form}Form.Itemlabel标题内容nametextrules{[{ required: true, message: 请输入标题内容 }]}Input //Form.ItemForm.Item label标题级别 namelevelSelectoptions{[{ value: 1, label: 一级标题 },{ value: 2, label: 二级标题 },{ value: 3, label: 三级标题 },]}//Form.ItemForm.Item nameisCenter valuePropNamecheckedCheckbox居中显示/Checkbox/Form.Item/Form)
}export default PropComponent 组件属性数据同步画布 
需求 要点 
componentProp 统一更新组件数据更新方式传递给 PropComponentRedux 设计 changeComponentProps 参数更新函数 
思路 
用户选择画布组件后传递 selectedId 到 Redux 中用户更新组件属性面板数值会通过 onChange 事件传递参数到 Redux采用 changeComponentProps 对画布中组件数据进行修改 
Edit/componentProp.tsx 
import React, { FC } from react
import useGetComponentInfo from ../../../hooks/useGetComponentInfo
import {getComponentConfByType,ComponentPropsType,
} from ../../../component/QuestionComponents
import { useDispatch } from react-redux
import { changeComponentProps } from ../../../store/componentReducerconst NoProp  ()  {return div style{{ textAlign: center }}请先选择组件/div
}const ComponentProp: FC  ()  {const dispatch  useDispatch()const { selectedComponent }  useGetComponentInfo()if (!selectedComponent) return NoProp /const { type, props }  selectedComponentconst componentConf  getComponentConfByType(type)if (!componentConf) return NoProp /const { PropComponent }  componentConf// 组件参数更新传递组件参数到 Redux 进行更新function changeProps(newProps: ComponentPropsType) {if (!selectedComponent) returndispatch(changeComponentProps({ fe_id: selectedComponent.fe_id, newProps }))}return PropComponent {...props} onChange{changeProps} /
}
export default ComponentProp 
store/componentReducer/index.tsx 
import { createSlice, PayloadAction } from reduxjs/toolkit
import { ComponentPropsType } from ../../component/QuestionComponents......
export const componentsSlice  createSlice({name: component,initialState: INIT_STATE,reducers: {......changeComponentProps: (draft: ComponentsStateType,action: PayloadAction{ fe_id: string; newProps: ComponentPropsType })  {const { fe_id, newProps }  action.payloadconst component  draft.componentList.find(c  c.fe_id  fe_id)if (component) {component.props  {...component.props,...newProps,}}},},
})export const {resetComponentList,changeSelectedId,addComponent,changeComponentProps,
}  componentsSlice.actions
export default componentsSlice.reducer