目标
问卷编辑器的开发
设计UI - 拆分布局
水平垂直居中
画布 y方向滚动
自定义问卷组件
后端 返回组件数据
js
//获取单个问卷信息
{
url: '/api/question/:id',
method: 'get',
response: () => {
return {
errno: 0,
data: {
id: Random.id(),
title: Random.ctitle(),
componentList:[
//Title
{
id:Random.id(),
type:'questionTitle', //组件类型不能重复,前后端统一
title:"标题",
props:{
text:"问卷标题",
level:1,
isCenter:false
}
},
//Input
{
id:Random.id(),
type:'questionInput',
title:"输入框",
props:{
title:"输入框",
placeholder:"请输入内容",
}
},
//Input2
{
id:Random.id(),
type:'questionInput',
title:"输入框2",
props:{
title:"输入框2",
placeholder:"请输入内容2",
}
}
]
}
}
}
},
前端 redux 存储后端返回组件数据
切片
js
import { createSlice , PayloadAction } from "@reduxjs/toolkit";
import { ComponentPropsType } from "../../components/QuestionComponents";
//单个组件的信息
export type ComponentInfoType={
fe_id : string,//为什么下划线
type : string,
title: string,
props:ComponentPropsType
}
//redux存放组件列表
//1. 定义数据结构
export type ComponentsStateType={
componentList:Array<ComponentInfoType>,
}
//2. 初始化
const INIT_STATE:ComponentsStateType={
componentList:[],
//其他扩展
}
export const componentsSlice = createSlice({
name:"components",
initialState:INIT_STATE,
reducers:{
//重置所有组件
//看不懂啊老铁!!!!
resetComponentList:(state: ComponentsStateType , action: PayloadAction<ComponentsStateType>)=>{
return action.payload
}
}
})
//导出所有的actions
export const {resetComponentList} = componentsSlice.actions
export default componentsSlice.reducer
store
js
import { configureStore } from '@reduxjs/toolkit'
import userReducer, { UserStateType } from './userReducer'
import componentsReducer , {ComponentsStateType}from './componentsReducer'
export type StateType={
user : UserStateType,
components : ComponentsStateType
}
export default configureStore({
reducer: {
//分模块注册
user: userReducer, // 存储user数据
components : componentsReducer// 存储问卷组件列表的数据
// 存储问卷组件列表的数据
// 存储问卷信息数据
}
})
发请求时存储数据
js
import { useEffect , useState } from "react";
import { useParams } from "react-router-dom";
import { useRequest } from "ahooks";
import {useDispatch} from 'react-redux'
import {resetComponentList} from '../store/componentsReducer'
//导入发起请求的函数
import { getQuestinService } from "../services/question";
function useLoadQuestionData() {
const dispatch = useDispatch()
const {id = ''} =useParams()
const {data , loading , error , run} = useRequest(
async (id : string) => {
if(!id) throw new Error('不存在问卷id')
const data = await getQuestinService(id)
return data
},
{
manual: true,
}
)
//根据获取的data 设置redux store
useEffect(() => {
if(!data) return
const {title ='' , componentList = []} = data
//获取到的componentList 存储到 Redux store中
dispatch(resetComponentList({
componentList
}))
},[data])
//问卷改变时, 重新加载问卷数据
useEffect(() => {
run(id)
},[id])
return {
loading,
error,
}
}
export default useLoadQuestionData;
页面画布区域显示组件列表
自定义hook获取数据
js
import { useSelector } from "react-redux";
import { StateType } from "../store";
import { ComponentsStateType } from "../store/componentsReducer";
function useGetComponentInfo() {
//使用useSelector获取store中的数据
const componens= useSelector<StateType>(state => state.components) as ComponentsStateType
//结构出空数组
const {componentList = []} = componens
return {
componentList
}
}
export default useGetComponentInfo;
重构canvas页面
js
import { FC } from 'react';
import styles from './EditCanvas.module.scss';
//静态展示
import useGetComponentInfo from '../../../hooks/useGetComponentInfo';
import { ComponentInfoType } from '../../../store/componentsReducer';
import { getComponentConfByType } from '../../../components/QuestionComponents';
type PropsType={
loading : boolean
}
const EditCanvas: FC<PropsType> = ({loading}) => {
const {componentList } = useGetComponentInfo();
if(loading){
return <div>loading</div>
}
//根据传入的组件 ,
function getComponent(componetInfo : ComponentInfoType)
{
const {type , props} = componetInfo
//根据组件类型找到对应的组件配置
const componentConf= getComponentConfByType(type)
if(!componentConf) return null
const {Component} = componentConf
return <Component {...props} />
}
return (
<div className={styles.canvas}>
{componentList.map(c => {
const {id} = c
return (
<div key={id} className={styles['component-warpper']}>
<div className={styles.component}>
{getComponent(c)}
</div>
</div>
)
})}
</div>
);
};
export default EditCanvas;
点击组件选中效果
添加selectedId,点击时,修改当前选中组件id
js
import { createSlice , PayloadAction } from "@reduxjs/toolkit";
import { ComponentPropsType } from "../../components/QuestionComponents";
import {produce} from "immer";
//单个组件的信息
export type ComponentInfoType={
// fe_id : string,//为什么是fe_id
id: string
type : string,
title: string,
props:ComponentPropsType
}
//redux存放组件列表
//1. 定义数据结构
export type ComponentsStateType={
componentList:Array<ComponentInfoType>,
selectedId:string
}
//2. 初始化
const INIT_STATE:ComponentsStateType={
selectedId:'',
componentList:[],
//其他扩展
}
export const componentsSlice = createSlice({
name:"components",
initialState:INIT_STATE,
reducers:{
//1. 重置所有组件
//看不懂啊老铁!!!!
resetComponentList:(state: ComponentsStateType , action: PayloadAction<ComponentsStateType>)=>{
return action.payload
},
//2.修改选中的组件id
//使用immer , 改进state不可变数据的写法
changeSelctedId:produce((state: ComponentsStateType , action: PayloadAction<string>)=>{
state.selectedId=action.payload
})
}
})
//导出所有的actions
export const {resetComponentList} = componentsSlice.actions
export default componentsSlice.reducer
页面注册点击事件
js
//点击选中组件
function handleClick(id: string) {
dispatch(changeSelctedId(id))
}
点击后改变样式
classsNames css样式的拼接
js
import classNames from 'classnames'; // 这个是实现css样式的拼接
{componentList.map(c => {
const {id} = c
//拼接classname
const defaultClassName=styles['component-warpper']
const selectedClassName=styles.selected
const wrapperClassName = classNames({
[defaultClassName]: true,
[selectedClassName]: id === selectedId
})
return (
<div onClick={()=>{handleClick(id)}} key={id} className={wrapperClassName}>
<div className={styles.component}>
{getComponent(c)}
</div>
</div>
)
})}
点击空白处,取消选中效果
注意这里阻止冒泡的操作
默认初始加载时选择第一个组件
组件库
组件分组
left 布局搭建
选取 组件库中的 tabs组件
又提取出组件 componentlib
显示到组件库
点击,添加组件到画布
画布信息需要更新
画布信息存在Redux中
处理redux
页面中使用
组件属性面板
组件属性的配置
每个组件的属性不一样,单独配置
js
import React, {FC, useEffect} from "react";
import { QuestionInputPropsType } from "./interface";
//引入组件库
import {Form ,Input} from "antd";
const PropComponent:FC<QuestionInputPropsType> = (props:QuestionInputPropsType) => {
const {title , placeholder} = props;
return (
<div>
<Form
layout="vertical"
initialValues={{ title ,placeholder }}
>
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
<Input />
</Form.Item>
<Form.Item label="Placeholder" name="placeholder">
<Input/>
</Form.Item>
</Form>
</div>
)
}
export default PropComponent;
画布中点击不同的组件时,监听变化,即时显示在右侧属性栏
组件配置中引入属性配置
属性面板显示组件属性
根据selectedID面板显示组件属性
onchange改变组件属性时,同步到Redux Store
改变组件属性时,统一交给上层的componentProops管理
在redux store 中增加修改属性的方法
头部编辑栏
定义组件
页面中引入
js
import React , {FC} from "react";
import styles from "./EditHeader.module.scss";
import {
Button,
Space,
Typography,
}from 'antd';
import { LeftOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
const {Title} = Typography;
const EditHeader:FC = ()=>{
const nav = useNavigate()
return (
<div className={styles['header-wrapper']}>
<div className={styles.header}>
<div className={styles.left}>
<Space>
<Button type="link" icon={<LeftOutlined></LeftOutlined>} onClick={()=>nav(-1)}>返回</Button>
<Title>问卷标题</Title>
</Space>
</div>
<div className={styles.main}>
中
</div>
<div className={styles.right}>
<Space>
<Button>保存</Button>
<Button>发布</Button>
</Space>
</div>
</div>
</div>
)
}
export default EditHeader;