吐司问卷(React低代码):问卷编辑器 II

协同平台前端技术全景(九):低代码表单与流程引擎 某装备制造企业协同平台采用双引擎低代码架构:VForm3(本地UMD包)实现动态表单,自研EasyProcess管理审批流程,二者通过formFieldData无缝联动。表单模板可配置、无需发版,真正实现“改表单不改代码”,显著提升业务响应效率。 阅读详情

吐司问卷:问卷编辑器 II

Date: February 26, 2025


Log

**软件设计的可拓展性:**对修改封闭,对拓展开放




工具栏

删除组件

需求:

2025-02-27 00.04.23.gif

要点:

  • 实现删除选中组件
    • 思路:重新计算 selectedId,优先选择下一个,没有下一个则选择上一个
    • 以上通过componentReducer工具函数utils

componentReducer/index.ts

removeSelectedComponent: (draft: ComponentsStateType) => {
  const { selectedId: removeId, componentList } = draft
  // 重新计算 selectedId, 优先选择下一个,没有下一个则选择上一个
  const nextSelectedId = getNextSelectedId(removeId, componentList)
  draft.selectedId = nextSelectedId
  // 删除组件
  const index = componentList.findIndex(c => c.fe_id === removeId)
  componentList.splice(index, 1)
},

componentReducer/utils.ts

import { ComponentInfoType } from './index'

/**
 * 获取下一个选中的组件 id
 * @param fe_id 当前选中的组件 id
 * @param componentList 组件列表
 * @returns 下一个选中的组件 id
 */
export function getNextSelectedId(
  fe_id: string,
  componentList: ComponentInfoType[]
) {
  const index = componentList.findIndex(c => c.fe_id === fe_id)
  if (index < 0) return ''
  if (index === componentList.length - 1) {
    return componentList[index - 1].fe_id
  } else {
    return componentList[index + 1].fe_id
  }
}


隐藏/显示组件

需求:

2025-02-27 01.01.39.gif

要点:

  • 定义属性 isHidden(Mock + Redux store)
  • Redux中changeComponentHidden修改 isHidden,实现 显示/隐藏 功能
    • componentList更新后过滤掉隐藏的组件
  • 修复潜在问题:隐藏组件属性暴露

思路:

componentReducer 中先定义属性 isHidden ,Redux 中实现 changeComponentHidden

用于修改 isHidden,从而实现 显示/隐藏功能。不过,记得页面的 componentList 需要过滤掉隐藏的信息,并且在处理组件对应属性面板的时候,也得先过滤掉隐藏的信息,再做选中下个组件逻辑。

潜在问题:

当组件A上面有隐藏组件B时,隐藏组件A,右侧的组件属性面板会显示B的属性。

2025-02-27 00.53.00.gif

参考服务端的 Mock 数据如下:

{
  fe_id: Random.id(),
  type: 'questionInput',
  title: '这是一个输入框组件',
  isHidden: false,
  props: {
    title: '你的电话',
    placeholder: '请输入内容'
  }
},
{
  fe_id: Random.id(),
  type: 'questionInput',
  title: '这是一个输入框组件',
  isHidden: true,
  props: {
    title: '隐藏咯!!!',
    placeholder: '请输入内容'
  }
},
{
  fe_id: Random.id(),
  type: 'questionInput',
  title: '这是一个输入框组件',
  isHidden: false,
  props: {
    title: '上面有一个隐藏元素',
    placeholder: '请输入内容'
  }
}

EditCanvas.tsx 组件列表更新后去除隐藏组件

  }
  return (
    <div className={styles.canvas}>
      {componentList
        .filter(c => !c.isHidden)
        .map(c => {
          const { fe_id } = c
          // 拼接 class name
          const wrapperDefaultClassName = styles['component-wrapper']

componentReducer/index.ts Redux实现隐藏组件

export type ComponentInfoType = {
  fe_id: string
  type: string
  title: string
  isHidden?: boolean
  props: ComponentPropsType
}

changeComponentHidden: (
  draft: ComponentsStateType,
  action: PayloadAction<{ fe_id: string; isHidden: boolean }>
) => {
  const { componentList } = draft
  const { fe_id, isHidden } = action.payload
  const component = draft.componentList.find(c => c.fe_id === fe_id)
  // 重新计算 selectedId, 优先选择下一个,没有下一个则选择上一个
  let newSelectedId = ''
  if (isHidden) {
    newSelectedId = getNextSelectedId(fe_id, componentList)
  } else {
    newSelectedId = fe_id
  }
  draft.selectedId = newSelectedId
  if (component) {
    component.isHidden = isHidden
  }
},

componentReducer/utils.ts 重新计算 selected 时需要过滤隐藏元素

import { ComponentInfoType } from './index'

/**
 * 获取下一个选中的组件 id
 * @param fe_id 当前选中的组件 id
 * @param componentList 组件列表
 * @returns 下一个选中的组件 id
 */
export function getNextSelectedId(
  fe_id: string,
  componentList: ComponentInfoType[]
) {
	// 重新计算 selected 时需要过滤隐藏元素
  const visibleComponentList = componentList.filter(c => !c.isHidden)
  const index = visibleComponentList.findIndex(c => c.fe_id === fe_id)
  if (index < 0) return ''
  if (index === visibleComponentList.length - 1) {
    return visibleComponentList[index - 1].fe_id
  } else {
    return visibleComponentList[index + 1].fe_id
  }
}



锁定/解锁组件

需求:

2025-02-27 13.56.02.gif

思路:

分析需求:

当点击锁定按钮时,可能需要传递锁定这个参数,因此,先从数据层面入手:

数据层面:先为组件参数定义 isLocked 属性,并在 Redux 中设计 锁定逻辑

逻辑层面:定位到顶部的工具栏,获取 Redux 中的锁定函数,并绑定到对应组件。

样式层面:当点击实现锁定效果

另外,当点击对应组件,属性面板组件也需要锁定,这一块也得需要分析:

先从数据层面入手

数据层面:表单锁定,根据 AntD,可能需要 disable 的属性,因此我们需要为属性面板的组件添加 disabled 的参数设定。

逻辑层面:点击画布中组件时,传递 isHidden 到属性组件中,也就是属性面板,如果画布中组件是锁定的,那么我们就传递 disable 给组件对应的属性面板。

样式层面:给表单添加 disabled 属性即可。

要点:

  • 数据:
    • 定义属性 isLocked(Mock + Redux store)
  • 变化:
    • 面板组件锁定:定义 Redux 中 toggleComponentLock 处理锁定
    • 组件属性面板锁定:属性面板,组件锁定则禁用 form
  • 样式:
    • 画布:增加 locked 样式
    • 属性面板组件锁定

Code:

componentReducer/index.ts

toggleComponentLock: (
  draft: ComponentsStateType,
  action: PayloadAction<{ fe_id: string }>
) => {
  const { fe_id } = action.payload
  const component = draft.componentList.find(c => c.fe_id === fe_id)
  if (component) {
    component.isLocked = !component.isLocked
  }
},

样式:

EditCanvas.tsx

<div className={styles.canvas}>
  {componentList
    .filter(c => !c.isHidden)
    .map(c => {
      const { fe_id, isLocked } = c
      // 样式处理
      const wrapperDefaultClassName = styles['component-wrapper']
      const selectedClassName = styles.selected
      const locked = styles.locked
      const wrapperClassName = classNames({
        [wrapperDefaultClassName]: true,
        [selectedClassName]: fe_id === selectedId,
        [locked]: isLocked,
      })

      return (
        <div
          key={fe_id}
          className={wrapperClassName}
          onClick={e => handleClick(e, fe_id || '')}
        >
          <div className={styles.component}>{getComponent(c)}</div>
        </div>
      )
    })}
</div>

EditCanvas.module.scss

.locked {
  opacity: 0.5;
  cursor: not-allowed;
}

属性面板,组件锁定则禁用 form

componentProp.tsx

<PropComponent
  {...props}
  disabled={isLocked || isHidden}
  onChange={changeProps}
/>


复制/粘贴组件

需求:

2025-02-27 15.57.08.gif

要点:

  • 在 Redux store 中存储复制的内容 copiedComponent
  • 粘贴按钮,判断是否 disabled
  • 公共代码抽离 insertNewComponent :新增组件逻辑

思路:

需求:点击组件,然后点击复制按钮,再选择位置,后点击粘贴,将拷贝的组件插入对应位置。

数据层面:

  • 组件状态需要新增 copiedComponent 状态,用于处理粘贴。

逻辑层面:

  • 选中组件,再点击复制按钮,将 selected 传递到 redux 中
  • Redux中设定 拷贝和粘贴 函数,根据 selectedId 深度拷贝对应组件,然后生成具有新的id的深拷贝组件,最后插入到对应位置即可。

utils.ts

/**
 * 插入新组件
 * @param draft 组件状态
 * @param newCompontent 新组件
 * @returns
 */
export const insertNewComponent = (
  draft: ComponentsStateType,
  newCompontent: ComponentInfoType
) => {
  const { selectedId, componentList } = draft
  const 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
}

componentReducer/index.ts

export type ComponentsStateType = {
  selectedId: string
  componentList: Array<ComponentInfoType>
  copiedComponent: ComponentInfoType | null
}

const INIT_STATE: ComponentsStateType = {
  selectedId: '',
  componentList: [],
  copiedComponent: null,
}

------

copySelectedComponent: (draft: ComponentsStateType) => {
  const { selectedId, componentList } = draft
  const selectedComponent = componentList.find(c => c.fe_id === selectedId)
  if (selectedComponent) {
    draft.copiedComponent = clonedeep(selectedComponent)
  }
},
pasteCopiedComponent: (draft: ComponentsStateType) => {
  const { copiedComponent } = draft
  if (!copiedComponent) return
  const newCopiedComponent = clonedeep(copiedComponent)
  newCopiedComponent.fe_id = nanoid()
  insertNewComponent(draft, newCopiedComponent)
},


画布增加快捷键

需求:

2025-02-27 17.43.54.gif

要点:

  • 删除、复制、粘贴、上下选中功能
  • 处理潜在问题:属性面板进行 backspace 时,会删除画布组件

**潜在问题:**属性面板进行 backspace 时,会删除画布组件

2025-02-27 16.45.31.gif

解决方案:

点击input组件显示的时候 <input … />,点击其他组件,比如画布组件会显示

根据以上这点,来处理删除快捷键问题。

function isActiveElementValid() {
  const activeElement = document.activeElement
  // 光标没有 focus 到 ipnut 上
  if (activeElement === document.body) {
    return true
  }
  return false
}

useBindCanvasKeyPress.tsx

import { useDispatch } from 'react-redux'
import {
  removeSelectedComponent,
  copySelectedComponent,
  pasteCopiedComponent,
  selectPrevComponent,
  selectNextComponent,
} from '../store/componentReducer'
import { useKeyPress } from 'ahooks'

/**
 * 判断光标是否在 input 上
 * @returns
 *  true: 光标在 input 上
 *  false: 光标不在 input 上
 *
 */
function isActiveElementValid() {
  const activeElement = document.activeElement
  // 光标没有 focus 到 ipnut 上
  if (activeElement === document.body) {
    return true
  }
  return false
}

const useBindCanvasKeyPress = () => {
  const dispatch = useDispatch()
  // 删除选中的组件
  useKeyPress(['Delete', 'backspace'], () => {
    if (!isActiveElementValid()) return
    dispatch(removeSelectedComponent())
  })
  // 复制选中的组件
  useKeyPress(['ctrl.c', 'meta.c'], () => {
    if (!isActiveElementValid()) return
    dispatch(copySelectedComponent())
  })
  // 粘贴复制的组件
  useKeyPress(['ctrl.v', 'meta.v'], () => {
    if (!isActiveElementValid()) return
    dispatch(pasteCopiedComponent())
  })
  // 选中上一个组件
  useKeyPress(['uparrow'], () => {
    if (!isActiveElementValid()) return
    dispatch(selectPrevComponent())
  })
  // 选中下一个组件
  useKeyPress(['downarrow'], () => {
    if (!isActiveElementValid()) return
    dispatch(selectNextComponent())
  })
}

export default useBindCanvasKeyPress

componentReducer.tsx

  selectPrevComponent: (draft: ComponentsStateType) => {
    const { selectedId, componentList } = draft
    const index = componentList.findIndex(c => c.fe_id === selectedId)
    // 如果是第一个组件,不做任何操作
    if (index <= 0) return
    const prevComponent = componentList[index - 1]
    if (prevComponent) {
      draft.selectedId = prevComponent.fe_id
    }
  },
  selectNextComponent: (draft: ComponentsStateType) => {
    const { selectedId, componentList } = draft
    const index = componentList.findIndex(c => c.fe_id === selectedId)
    if (index <= 0) return
    if (index === componentList.length - 1) return
    const nextComponent = componentList[index + 1]
    if (nextComponent) {
      draft.selectedId = nextComponent.fe_id
    }
  },



组件库拓展设计

扩展性:

  • 从最简单的组件开始
  • 定义好规则,跑通流程
  • 增加其他组件,不改变编辑器的规则

**软件开发规则:**对拓展开放,对修改封闭



段落组件

需求:

2025-02-28 00.56.19.gif

要点:

  • 段落组件类型、接口、组件、属性组件实现
  • 潜在问题:段落换行处理

文件树:

│   │   ├── QuestionComponents
│   │   │   ├── QuestionParagraph
│   │   │   │   ├── Component.tsx
│   │   │   │   ├── PropComponent.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── interface.ts
│   │   │   └── index.ts

潜在问题:段落换行处理

尽量不要使用 dangerouslySetInnerHTML 来渲染 html,会有 xss 攻击风险。

如下可以选用 map 对组件列表进行渲染

const textList = text.split('\n')
<Paragraph
  style={{ textAlign: isCenter ? 'center' : 'start', marginBottom: 0 }}
>
  {/* <span dangerouslySetInnerHTML={{ __html: t }}></span> */}
  {textList.map((item, index) => (
    <span key={index}>
      {index === 0 ? '' : <br />}
      {item}
    </span>
  ))}
</Paragraph>

Component.tsx

import React, { FC } from 'react'
import {
  QuestionParagraphPropsType,
  QuestionParagraphDefaultProps,
} from './interface'
import { Typography } from 'antd'

const { Paragraph } = Typography

const Component: FC<QuestionParagraphPropsType> = (
  props: QuestionParagraphPropsType
) => {
  const { text = '', isCenter = false } = {
    ...QuestionParagraphDefaultProps,
    ...props,
  }
  // 尽量不要使用 dangerouslySetInnerHTML 来渲染 html,会有 xss 攻击风险
  // const t = text.replace('\n', '<br/>')
  const textList = text.split('\n')
  return (
    <Paragraph
      style={{ textAlign: isCenter ? 'center' : 'start', marginBottom: 0 }}
    >
      {/* <span dangerouslySetInnerHTML={{ __html: t }}></span> */}
      {textList.map((item, index) => (
        <span key={index}>
          {index === 0 ? '' : <br />}
          {item}
        </span>
      ))}
    </Paragraph>
  )
}

export default Component

index.ts

/**
 *  @description 段落组件
 */
import Component from './Component'
import { QuestionParagraphDefaultProps } from './interface'
import PropComponent from './PropComponent'
export * from './interface'
// paragraph 组件配置
export default {
  title: '段落',
  type: 'questionPragraph',
  Component: Component,
  PropComponent: PropComponent,
  defaultProps: QuestionParagraphDefaultProps,
}

interface.ts

export type QuestionParagraphPropsType = {
  text?: string
  isCenter?: boolean
  onChange?: (newProps: QuestionParagraphPropsType) => void
  disabled?: boolean
}

export const QuestionParagraphDefaultProps: QuestionParagraphPropsType = {
  text: '一行段落',
  isCenter: false,
}

PropComponent.tsx

import React, { FC } from 'react'
import { useEffect } from 'react'
import { Form, Input, Checkbox } from 'antd'
import { QuestionParagraphPropsType } from './interface'

const { TextArea } = Input
const PropComponent: FC<QuestionParagraphPropsType> = (
  props: QuestionParagraphPropsType
) => {
  const { text, isCenter, onChange, disabled } = props
  const [form] = Form.useForm()
  useEffect(() => {
    form.setFieldsValue({ text, isCenter })
  }, [text, isCenter])
  function handleValuesChange() {
    if (onChange) {
      onChange(form.getFieldsValue())
    }
  }
  return (
    <Form
      layout="vertical"
      initialValues={{ text, isCenter }}
      form={form}
      onChange={handleValuesChange}
      disabled={disabled}
    >
      <Form.Item
        label="段落内容"
        name="text"
        rules={[{ required: true, message: '请输入段落内容' }]}
      >
        <TextArea cols={5} />
      </Form.Item>
      <Form.Item label="是否居中" name="isCenter" valuePropName="checked">
        <Checkbox />
      </Form.Item>
    </Form>
  )
}

export default PropComponent



单选框组件

多选框同理处理

需求:

2025-03-02 21.10.26.gif

要点:

Component.tsx

import React from 'react'
import { Radio, Typography } from 'antd'
import { QuestionRadioPropsType, QuestionRadioDefaultProps } from './interface'

const { Paragraph } = Typography
const QuestionRadio: React.FC<QuestionRadioPropsType> = (
  props: QuestionRadioPropsType
) => {
  const { title, isVertical, options, value } = {
    ...QuestionRadioDefaultProps,
    ...props,
  }

  const radioStyle: React.CSSProperties = isVertical
    ? { display: 'flex', flexDirection: 'column' }
    : {}

  return (
    <div>
      <Paragraph strong>{title}</Paragraph>
      <Radio.Group
        value={value}
        style={radioStyle}
        options={options?.map(option => ({
          value: option.value,
          label: option.text,
        }))}
      />
    </div>
  )
}
export default QuestionRadio

PropCompnent.tsx

import React, { FC } from 'react'
import { useEffect } from 'react'
import { Checkbox, Form, Input, Button, Space, Select } from 'antd'
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
import { QuestionRadioPropsType, OptionType } from './interface'
import { nanoid } from '@reduxjs/toolkit'

const PropComponent: FC<QuestionRadioPropsType> = (
  props: QuestionRadioPropsType
) => {
  const { title, isVertical, options, value, disabled, onChange } = props
  const [form] = Form.useForm()

  useEffect(() => {
    form.setFieldsValue({ title, isVertical, options, value })
  }, [title, isVertical, options, value])

  function handleValuesChange() {
    const values = form.getFieldsValue()
    const { options } = values
    // 生成唯一的value
    if (options && options.length > 0) {
      options.forEach((opt: OptionType) => {
        if (!opt.value) {
          opt.value = nanoid(5)
        }
      })
    }
    if (onChange) {
      onChange(form.getFieldsValue())
    }
  }
  return (
    <Form
      layout="vertical"
      initialValues={{ title, isVertical, options, value }}
      onValuesChange={handleValuesChange}
      form={form}
      disabled={disabled}
    >
      <Form.Item
        label="标题"
        name="title"
        rules={[{ required: true, message: '请输入标题' }]}
      >
        <Input />
      </Form.Item>
      <Form.Item label="选项" shouldUpdate>
        <Form.List name="options">
          {(fields, { add, remove }) => (
            <>
              {fields.map(({ key, name }) => (
                <Space key={key} align="baseline">
                  <Form.Item
                    name={[name, 'text']}
                    rules={[
                      { required: true, message: '请输入选项文字' },
                      {
                        validator: (_, value) => {
                          const optionTexts = form
                            .getFieldValue('options')
                            .map((opt: OptionType) => opt.text)
                          if (
                            optionTexts.filter((text: string) => text === value)
                              .length > 1
                          ) {
                            return Promise.reject(new Error('选项重复!'))
                          }
                          return Promise.resolve()
                        },
                      },
                    ]}
                  >
                    <Input placeholder="选项文字" />
                  </Form.Item>
                  <MinusCircleOutlined onClick={() => remove(name)} />
                </Space>
              ))}
              <Form.Item>
                <Button
                  type="dashed"
                  onClick={() => add({ text: '', value: '' })}
                  block
                  icon={<PlusOutlined />}
                >
                  添加选项
                </Button>
              </Form.Item>
            </>
          )}
        </Form.List>
      </Form.Item>
      <Form.Item label="默认选中" name="value">
        <Select
          options={options?.map(({ text, value }) => ({
            label: text,
            value: value,
          }))}
          allowClear
          placeholder="请选择默认选项"
        />
      </Form.Item>
      <Form.Item label="竖向排列" name="isVertical" valuePropName="checked">
        <Checkbox />
      </Form.Item>
    </Form>
  )
}
export default PropComponent


表单细节:getFieldValue

在这两个文件中,getFieldValue 的使用方式不同是因为它们获取表单字段值的方式不同。

文件 PropComponent.tsx

const optionTexts = form
.getFieldsValue()
.list.map((opt: OptionType) => opt.text)

在这个文件中,getFieldsValue 被用来获取整个表单的所有字段值,然后通过链式调用获取 list 字段的值。list 是一个数组,其中包含了所有选项的对象。

文件 PropComponent.tsx-1

const optionTexts = form
.getFieldValue('options')
.map((opt: OptionType) => opt.text)

在这个文件中,getFieldValue 被用来直接获取 options 字段的值。options 是一个数组,其中包含了所有选项的对象。

总结

  • getFieldsValue 返回整个表单的所有字段值作为一个对象。
  • getFieldValue 需要一个参数,返回指定字段的值。

这两种方法的选择取决于你需要获取的字段值的范围。如果你只需要一个特定字段的值,使用 getFieldValue 更加直接和高效。如果你需要多个字段的值,使用 getFieldsValue 会更方便。


fix: 重复选项提示处理

需求:

image.png

注释代码运行时候,当用户添加选项,输入选项值时后,哪怕值与之前选项不重复,它会保持报“选项重复!

<Form.Item
  name={[name, 'text']}
  rules={[
    { required: true, message: '请输入选项文字' },
    {
      validator: (_, value) => {
        const optionTexts = form
          .getFieldValue('list')
          .map((opt: OptionType) => opt.text)
        // if (optionTexts.indexOf(value) !== -1) {
        //   return Promise.reject('选项文字不能重复')
        // }
        if (
          optionTexts.filter((text: string) => text === value)
            .length > 1
        ) {
          return Promise.reject(new Error('选项重复!'))
        }
        return Promise.resolve()
      },
    },
  ]}
>
  <Input />
</Form.Item>

问题原因:

  • optionTexts 包含当前正在编辑选项的旧值

  • 当用户开始输入新值时,表单立即更新导致:

    旧值仍然存在于数组中,新值会被重复校验,即使输入唯一值,旧值的存在也会触发校验失败

解决方案:

if (
  optionTexts.filter((text: string) => text === value)
    .length > 1
) {
  return Promise.reject(new Error('选项重复!'))
}

校验逻辑解析:

  1. filter 会遍历所有选项文字(包含当前正在编辑的选项)
  2. 当相同文字出现 超过1次 时才触发错误
  3. 这意味着:
    • 允许当前编辑项自身存在一次
    • 只有当其他选项存在相同文字时才会报错
    • 空值场景:多个空选项会触发错误(因为 "" === "")
// 原错误逻辑(任意重复即报错,包含自身)
if (optionTexts.indexOf(value) !== -1) { ... }
// 当前逻辑(允许自身存在一次)
if (重复次数 > 1) { ... }
基于React技术栈的问卷星仿制项目设计源码 问卷星仿制项目:基于React技术栈开发,包含27个文件,包括5个JSON配置文件、3个JavaScript文件、3个TSX文件、3个TypeScript文件、2个PNG图像文件、2个CSS样式文件、1个.gitignore文件、1个husky/commit-msg文件、1个husky/pre-commit文件和1个Markdown文档。该项目是一个复杂低代码项目,采用最新React技术栈,旨在仿制问卷星平台,为用户提供一个类似的问卷调查解决方案。 立即下载

相关推荐

(源码)基于 React 框架的低代码生成器.zip

# 基于 React 框架的低代码生成器 ## 项目简介 本项目是基于 React 框架的低代码生成器,名为 lowcodemonorepo。它提供了一套强大的可视化工具,开发者可通过简单配置与拖拽操作,快速构建功能丰富的应用程序界面。项目包含编辑器、插件系统、骨架工作台等核心功能模块。 ## 项目的主要特性和功能 1. 模块化设计清晰的项目结构支持模块化开发,便于扩展与维护。 2. 可视化编辑器提供直观易用的可视化编辑体验,支持拖拽式操作。 3. 插件系统支持自定义插件,可扩展功能。 4. 丰富的内置资源包含多种内置组件和设置器,满足常见需求。 5. 预览功能具备画布面板和模拟器,方便预览与调试。 ## 安装使用步骤 ### 前提条件 确保已安装以下环境 Node.js(版本 16.16.0) pnpm(版本 7.x) React(版本 17.0.2) React DOM(版本 17.0.2) ### 安装依赖

基于React的开源低代码框架ToolJet

ToolJet 是用于构建基于 React 的 Web 应用程序和内部工具的开源低代码框架 🚀。

wbsu2004的博客 1万+

深入解析LCD DRM驱动框架中的KMS与GEM机制

本文深入解析了Linux内核中DRM(Direct Rendering Manager)驱动框架的核心机制,重点剖析了KMS(内核模式设置)与GEM(图形执行管理器)的工作原理。KMS作为显示系统的总指挥,负责原子化地控制CRTC、Encoder、Plane等硬件资源,确保画面同步更新,避免撕裂;GEM则作为显存大管家,通过DUMB和PRIME机制高效管理图形缓冲区的分配、共享与同步。文章结合RK3568平台实战,阐述了如何利用这些机制优化显示性能,并提供了调试技巧与常见问题解决方案。

weixin_29188997的博客 387

react可视化表单,低代码平台,表单生成器,逻辑跳转表单设计

form-active是新开发的可视化表单低代码平台,传统的表单设计只有拖拽组合的功能,form-active里面增加了逻辑关联、排序、常用组件存储等功能,能够更好的适用于日常需求。如果您还有其它使用场景,比如导出页面,保存数据等,还请耐心等待,后面会陆续的添加新的功能需求,希望在未来开发中,form-active表单设计 能成为你在表单设计的第一选择。

youjia727的博客 1308

C语言实验——各位数字之和排序

C语言实验——各位数字之和排序Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Discuss Problem Description给定n个正整数,根据各位数字之和从小到大进行排序。 Input输入数据有多组,每组数据占一行,每行的第一个数正整数n,表示整数个数,后面...

aiLMengi000的博客 1243

GIS国土工具实战:从地类分析到坐标转换,一站式解决项目难题

本文详细介绍了GIS国土工具在国土整治项目中的高效应用,涵盖地类分析、坐标转换、影像加载等核心功能。通过实战案例展示如何利用该工具快速处理shp与txt文件转换、智能比对三调二调数据,以及优化影像加载策略,显著提升项目效率。特别适合需要处理复杂国土数据的专业人员使用。

weixin_28732953的博客 334

基于React的低代码开发:探索应用构建的新模式

基于React的低代码开发:探索应用构建的新模式

“路漫漫其修远兮,吾将上下求索。” 2888

基于STM32与ESP8266的MQTT协议实现阿里云物联网平台远程LED控制

本文详细介绍了如何利用STM32单片机与ESP8266 Wi-Fi模块,通过MQTT协议连接阿里云物联网平台,实现远程控制LED灯的完整物联网项目。内容涵盖硬件连接、阿里云平台配置、MQTT协议与AT指令解析、STM32代码实战以及数据解析与调试技巧,为嵌入式开发者提供了一个清晰、实用的物联网入门实践指南。

svm4gardener的博客 791

基于React低代码平台开发:构建高效、灵活的应用新范式

本书由三部分组成,第 一部分介绍基础理论知识,第二部分介绍如何构建低代码系统,先进行需求分析,涉及列表页、详情页和表单页的布局需求和逻辑需求,可视化编辑器、应用管理、组件市场和用户管理的需求等内容,然后介绍设计思路,包含架构设计、Schema 设计、组件库市场和 MongoDB Document 设计等,z后介绍详细的编码过程,包括可视化编辑器的实现、渲染 SDK 的实现、版本控制,以及如何将其集成到其他系统。秦小倩(网名何遇)

程序边界 7319

il2cpp分析-gobal-metadata.dat解密

打开ida,把libil2cpp拖到ida中按快捷键Shift+F12等待字符串分析完后,搜索global-metadata.dat。ida中双击sub_57EE7C进入,VS code中也跳转到LoadMetadataFile函数。VSCode打开il2cpp源代码,全局搜索global-metadata.dat。工具:010Editor,IDA 7.5,Jadx,VS Code。判断加密逻辑为根据文件的长度循环读取字节码做亦或加密。双击搜索出来的结果,切记要选il2cpp目录下的。

Android_machong的博客 8024

基于任务的后勤智能体:三段构建与演进设计探究

这是系统智能化的核心难点。例如,对于一个“加强连级滩头阵地防御”子任务,系统会根据防御时长、预期敌军攻击强度、地形等因素,计算出所需的弹药(不同弹种的具体数量)、构筑工事的器材、人员的给养和饮水量、以及医疗救护的药品和血包数量。首先进行分词、词性标注等基础处理,然后利用在军事语料库上训练的命名实体识别(NER)模型,提取出关键实体,如任务类型(“夺控作战”)、作战对象(“Y岛”)、时间约束(“D日H时”、“24小时内”)、成功标准(“建立稳固滩头阵地”)、保障重点(“弹药充足”、“及时救治”)等。

weixin_49042442的博客 1088

低代码平台开发实践:基于React的高效构建与创新

低代码平台开发实践:基于React的高效构建与创新 随着技术的不断进步和业务需求的日益复杂,低代码平台成为现代软件开发领域中备受关注的工具之一。在这个快节奏的时代,基于React的低代码平台为开发者提供了一种高效构建应用程序的新途径。本文将深入探讨低代码平台在React上的开发实践,以及如何借助这一平台在项目中取得成功。

一键难忘的博客 7216

Carla自动驾驶仿真一:快速安装与运行Carla

网上有不少的Windows版本的Carla的安装教程,这里简单记录下自己安装过程,写的也比较详细,基本不用看其他文章。

zataji的博客 2万+

React 极简低代码实现

这篇文章讲解如何使用 React 开发一个简易的低代码生成器,实现拖拽生成页面的功能。作者使用 Next.js 搭建项目,并详细介绍了项目需求、组件设计以及核心功能的实现方式。文章中重点阐述了如何利用 react-dnd 和 react-moveable 这两个库来实现拖拽和元素移动,并分享了开发过程中遇到的难点和解决方案。最后,作者公开了项目的 GitHub 源码地址,方便读者学习和参考。

weixin_48191864的博客 1777

基于STM32L4XX的环境光传感器(AS7343-DLGMN)应用程序设计

define AS7343_STATUS_SPECTRAL_INT (1 << 3) /* 光谱数据就绪 */#define AS7343_SYS_CFG_SPECTRAL_EN (1 << 1) /* 光谱测量使能 */#define AS7343_SYS_CFG_ALS_EN (1 << 0) /* ALS使能 */#define AS7343_SYS_CFG_INT_EN (1 << 4) /* 中断使能 *//* 切换通道组 (0=组1 F1-F6, 1=组2 F7-F12) */

myron6688的博客 257

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

react 低代码图编辑探索

基于react的低代码可拖拽实现,低代码JsonSchema渲染实现,图可编辑实现

weixin_43783814的博客 1512

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

基于 React + Ant Design实现一个可拖拽布局的低代码编辑器渲染引擎(支持碰撞检测)

本文基于 React + Ant Design 打造一个类似 Retool 的组件拖拽布局渲染引擎,支持组件面板、画布编辑区、右侧配置区,并实现了 碰撞检测、虚线拖影、组件属性配置等交互细节。

qq_43771593的博客 1659

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

如何开发一个低代码编辑器(react)

结尾有源码。

weixin_45821809的博客 470

吐司问卷(React低代码):问卷编辑器 I

React低代码项目:问卷编辑器 I

CaptainDrake的博客 1311

从零开始实现一个简单的低代码编辑器

来自 「赵越」 同学的内部分享,文章示例代码:「https://github.com/shadowings-zy/mini-editor」,欢迎 Star。目录:编辑器功能拆分编辑器数据格式的定义项目代码结构重点逻辑的实现(画布渲染、属性联动、拖拽组件)与后台交互还可以优化的点「Oh, 对了,文末有福利!!!」一、写在前面低代码编辑器作为一种能够极大地提升开发效率的PaaS软件,近些年来一直收到各...

frontend_frank的博客 938

张铁柱-前端实现《低代码可视化编辑器》(一)思路整理 React-dnd+Ts

React、React-dnd、TypeScript构建前端低代码编辑器,包含拖拽,放置,排序等功能,持续更新中...

bizhiweiii的博客 735

从 0 到 1 实现低代码编辑器的基本功能

低代码编辑器的核心在于:用 JSON 结构描述页面,拖拽和属性编辑本质上是对 JSON 的增删改查,递归渲染则将 JSON 转化为真实页面。通过合理的数据结构设计、组件配置、状态管理和交互优化,可以实现一个功能完善、易于扩展的低代码平台。两个状态仓库的分离设计,使得页面结构与物料配置解耦,极大提升了系统的灵活性和可维护性。

2401_86373285的博客 984

吐司问卷(React低代码):用户登陆模块实现

JWT、登陆注册流程设计

CaptainDrake的博客 1419

基于React低代码平台开发:直击最新应用构建

随着数字化转型的深入,企业对应用开发的效率和灵活性要求越来越高。低代码平台(Low-Code Platform)作为一种新兴的软件开发方式,通过提供可视化的界面和预构建的组件,极大地简化了应用开发过程,降低了技术门槛。而基于React的低代码平台,更是凭借其组件化、响应式、数据驱动等特性,成为了当前低代码开发的热门选择。基于React的低代码平台开发有哪些优势、挑战以及实践方法呢?

哥的时代,为你保驾护航 3437

用 Vue/React 搭建简易低代码编辑器:组件拖拽原理

用 Vue/React 搭建简易低代码编辑器:组件拖拽原理

fruge365的博客 6452
上一篇: 吐司问卷(React低代码):问卷编辑器 I
下一篇: 吐司问卷(React低代码):用户登陆模块实现
CaptainDrake
博客等级 码龄6年 5363粉丝 187原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值