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

[复制链接]
发表于 2025-10-20 22:07:04 | 显示全部楼层 |阅读模式
吐司问卷:问卷编辑器 II

Date: February 26, 2025

Log

**软件操持的可拓展性:**对修改封闭,对拓睁开放



工具栏

删除组件

需求:

要点:


  • 实现删除选中组件

    • 思绪:重新盘算 selectedId,优先选择下一个,没有下一个则选择上一个
    • 以上通过componentReducer工具函数utils

componentReducer/index.ts
  1. removeSelectedComponent: (draft: ComponentsStateType) => {
  2.   const { selectedId: removeId, componentList } = draft
  3.   // 重新计算 selectedId, 优先选择下一个,没有下一个则选择上一个
  4.   const nextSelectedId = getNextSelectedId(removeId, componentList)
  5.   draft.selectedId = nextSelectedId
  6.   // 删除组件
  7.   const index = componentList.findIndex(c => c.fe_id === removeId)
  8.   componentList.splice(index, 1)
  9. },
复制代码
componentReducer/utils.ts
  1. import { ComponentInfoType } from './index'
  2. /**
  3. * 获取下一个选中的组件 id
  4. * @param fe_id 当前选中的组件 id
  5. * @param componentList 组件列表
  6. * @returns 下一个选中的组件 id
  7. */
  8. export function getNextSelectedId(
  9.   fe_id: string,
  10.   componentList: ComponentInfoType[]
  11. ) {
  12.   const index = componentList.findIndex(c => c.fe_id === fe_id)
  13.   if (index < 0) return ''
  14.   if (index === componentList.length - 1) {
  15.     return componentList[index - 1].fe_id
  16.   } else {
  17.     return componentList[index + 1].fe_id
  18.   }
  19. }
复制代码


隐蔽/表现组件

需求:

要点:


  • 界说属性 isHidden(Mock + Redux store)
  • Redux中changeComponentHidden修改 isHidden,实现 表现/隐蔽 功能

    • componentList更新后过滤掉隐蔽的组件

  • 修复埋伏标题:隐蔽组件属性袒露
思绪:
componentReducer 中先界说属性 isHidden ,Redux 中实现 changeComponentHidden
用于修改 isHidden,从而实现 表现/隐蔽功能。不外,记得页面的 componentList 必要过滤掉隐蔽的信息,而且在处置处罚组件对应属性面板的时间,也得先过滤掉隐蔽的信息,再做选中下个组件逻辑。
埋伏标题:
当组件A上面有隐蔽组件B时,隐蔽组件A,右侧的组件属性面板会表现B的属性。

参考服务端的 Mock 数据如下:
  1. {
  2.   fe_id: Random.id(),
  3.   type: 'questionInput',
  4.   title: '这是一个输入框组件',
  5.   isHidden: false,
  6.   props: {
  7.     title: '你的电话',
  8.     placeholder: '请输入内容'
  9.   }
  10. },
  11. {
  12.   fe_id: Random.id(),
  13.   type: 'questionInput',
  14.   title: '这是一个输入框组件',
  15.   isHidden: true,
  16.   props: {
  17.     title: '隐藏咯!!!',
  18.     placeholder: '请输入内容'
  19.   }
  20. },
  21. {
  22.   fe_id: Random.id(),
  23.   type: 'questionInput',
  24.   title: '这是一个输入框组件',
  25.   isHidden: false,
  26.   props: {
  27.     title: '上面有一个隐藏元素',
  28.     placeholder: '请输入内容'
  29.   }
  30. }
复制代码
EditCanvas.tsx 组件列表更新后去除隐蔽组件
  1.   }
  2.   return (
  3.     <div className={styles.canvas}>
  4.       {componentList
  5.         .filter(c => !c.isHidden)
  6.         .map(c => {
  7.           const { fe_id } = c
  8.           // 拼接 class name
  9.           const wrapperDefaultClassName = styles['component-wrapper']
复制代码
componentReducer/index.ts Redux实现隐蔽组件
  1. export type ComponentInfoType = {
  2.   fe_id: string
  3.   type: string
  4.   title: string
  5.   isHidden?: boolean
  6.   props: ComponentPropsType
  7. }
  8. changeComponentHidden: (
  9.   draft: ComponentsStateType,
  10.   action: PayloadAction<{ fe_id: string; isHidden: boolean }>
  11. ) => {
  12.   const { componentList } = draft
  13.   const { fe_id, isHidden } = action.payload
  14.   const component = draft.componentList.find(c => c.fe_id === fe_id)
  15.   // 重新计算 selectedId, 优先选择下一个,没有下一个则选择上一个
  16.   let newSelectedId = ''
  17.   if (isHidden) {
  18.     newSelectedId = getNextSelectedId(fe_id, componentList)
  19.   } else {
  20.     newSelectedId = fe_id
  21.   }
  22.   draft.selectedId = newSelectedId
  23.   if (component) {
  24.     component.isHidden = isHidden
  25.   }
  26. },
复制代码
componentReducer/utils.ts 重新盘算 selected 时必要过滤隐蔽元素
  1. import { ComponentInfoType } from './index'
  2. /**
  3. * 获取下一个选中的组件 id
  4. * @param fe_id 当前选中的组件 id
  5. * @param componentList 组件列表
  6. * @returns 下一个选中的组件 id
  7. */
  8. export function getNextSelectedId(
  9.   fe_id: string,
  10.   componentList: ComponentInfoType[]
  11. ) {
  12.         // 重新计算 selected 时需要过滤隐藏元素
  13.   const visibleComponentList = componentList.filter(c => !c.isHidden)
  14.   const index = visibleComponentList.findIndex(c => c.fe_id === fe_id)
  15.   if (index < 0) return ''
  16.   if (index === visibleComponentList.length - 1) {
  17.     return visibleComponentList[index - 1].fe_id
  18.   } else {
  19.     return visibleComponentList[index + 1].fe_id
  20.   }
  21. }
复制代码


锁定/解锁组件

需求:

思绪:
分析需求:
当点击锁定按钮时,大概必要转达锁定这个参数,因此,先从数据层面入手:
数据层面:先为组件参数界说 isLocked 属性,并在 Redux 中操持 锁定逻辑
逻辑层面:定位到顶部的工具栏,获取 Redux 中的锁定函数,并绑定到对应组件。
样式层面:当点击实现锁定效果
别的,当点击对应组件,属性面板组件也必要锁定,这一块也得必要分析:
先从数据层面入手
数据层面:表单锁定,根据 AntD,大概必要 disable 的属性,因此我们必要为属性面板的组件添加 disabled 的参数设定。
逻辑层面:点击画布中组件时,转达 isHidden 到属性组件中,也就是属性面板,假如画布中组件是锁定的,那么我们就转达 disable 给组件对应的属性面板。
样式层面:给表单添加 disabled 属性即可。
要点:


  • 数据:

    • 界说属性 isLocked(Mock + Redux store)

  • 变革:

    • 面板组件锁定:界说 Redux 中 toggleComponentLock 处置处罚锁定
    • 组件属性面板锁定:属性面板,组件锁定则禁用 form

  • 样式:

    • 画布:增长 locked 样式
    • 属性面板组件锁定

Code:
componentReducer/index.ts
  1. toggleComponentLock: (
  2.   draft: ComponentsStateType,
  3.   action: PayloadAction<{ fe_id: string }>
  4. ) => {
  5.   const { fe_id } = action.payload
  6.   const component = draft.componentList.find(c => c.fe_id === fe_id)
  7.   if (component) {
  8.     component.isLocked = !component.isLocked
  9.   }
  10. },
复制代码
样式:
EditCanvas.tsx
  1. <div className={styles.canvas}>
  2.   {componentList
  3.     .filter(c => !c.isHidden)
  4.     .map(c => {
  5.       const { fe_id, isLocked } = c
  6.       // 样式处理
  7.       const wrapperDefaultClassName = styles['component-wrapper']
  8.       const selectedClassName = styles.selected
  9.       const locked = styles.locked
  10.       const wrapperClassName = classNames({
  11.         [wrapperDefaultClassName]: true,
  12.         [selectedClassName]: fe_id === selectedId,
  13.         [locked]: isLocked,
  14.       })
  15.       return (
  16.         <div
  17.           key={fe_id}
  18.           className={wrapperClassName}
  19.           onClick={e => handleClick(e, fe_id || '')}
  20.         >
  21.           <div className={styles.component}>{getComponent(c)}</div>
  22.         </div>
  23.       )
  24.     })}
  25. </div>
复制代码
EditCanvas.module.scss
  1. .locked {
  2.   opacity: 0.5;
  3.   cursor: not-allowed;
  4. }
复制代码
属性面板,组件锁定则禁用 form
componentProp.tsx
  1. <PropComponent
  2.   {...props}
  3.   disabled={isLocked || isHidden}
  4.   onChange={changeProps}
  5. />
复制代码


复制/粘贴组件

需求:

要点:


  • 在 Redux store 中存储复制的内容 copiedComponent
  • 粘贴按钮,判断是否 disabled
  • 公共代码抽离 insertNewComponent :新增组件逻辑
思绪:
需求:点击组件,然后点击复制按钮,再选择位置,后点击粘贴,将拷贝的组件插入对应位置。
数据层面:


  • 组件状态必要新增 copiedComponent 状态,用于处置处罚粘贴。
逻辑层面:


  • 选中组件,再点击复制按钮,将 selected 转到达 redux 中
  • Redux中设定 拷贝和粘贴 函数,根据 selectedId 深度拷贝对应组件,然后天生具有新的id的深拷贝组件,末了插入到对应位置即可。
utils.ts
  1. /**
  2. * 插入新组件
  3. * @param draft 组件状态
  4. * @param newCompontent 新组件
  5. * @returns
  6. */
  7. export const insertNewComponent = (
  8.   draft: ComponentsStateType,
  9.   newCompontent: ComponentInfoType
  10. ) => {
  11.   const { selectedId, componentList } = draft
  12.   const index = componentList.findIndex(c => c.fe_id === selectedId)
  13.   if (index < 0) {
  14.     draft.componentList.push(newCompontent)
  15.   } else {
  16.     draft.componentList.splice(index + 1, 0, newCompontent)
  17.   }
  18.   draft.selectedId = newCompontent.fe_id
  19. }
复制代码
componentReducer/index.ts
  1. export type ComponentsStateType = {
  2.   selectedId: string
  3.   componentList: Array<ComponentInfoType>
  4.   copiedComponent: ComponentInfoType | null
  5. }
  6. const INIT_STATE: ComponentsStateType = {
  7.   selectedId: '',
  8.   componentList: [],
  9.   copiedComponent: null,
  10. }
  11. ------
  12. copySelectedComponent: (draft: ComponentsStateType) => {
  13.   const { selectedId, componentList } = draft
  14.   const selectedComponent = componentList.find(c => c.fe_id === selectedId)
  15.   if (selectedComponent) {
  16.     draft.copiedComponent = clonedeep(selectedComponent)
  17.   }
  18. },
  19. pasteCopiedComponent: (draft: ComponentsStateType) => {
  20.   const { copiedComponent } = draft
  21.   if (!copiedComponent) return
  22.   const newCopiedComponent = clonedeep(copiedComponent)
  23.   newCopiedComponent.fe_id = nanoid()
  24.   insertNewComponent(draft, newCopiedComponent)
  25. },
复制代码


画布增长速捷键

需求:

要点:


  • 删除、复制、粘贴、上下选中功能
  • 处置处罚埋伏标题:属性面板举行 backspace 时,会删除画布组件
**埋伏标题:**属性面板举行 backspace 时,会删除画布组件

办理方案:
点击input组件表现的时间 <input … />,点击其他组件,好比画布组件会表现
根据以上这点,来处置处罚删除快捷键标题。
  1. function isActiveElementValid() {
  2.   const activeElement = document.activeElement
  3.   // 光标没有 focus 到 ipnut 上
  4.   if (activeElement === document.body) {
  5.     return true
  6.   }
  7.   return false
  8. }
复制代码
useBindCanvasKeyPress.tsx
  1. 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() {
  2.   const activeElement = document.activeElement
  3.   // 光标没有 focus 到 ipnut 上
  4.   if (activeElement === document.body) {
  5.     return true
  6.   }
  7.   return false
  8. }
  9. 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
  1.   selectPrevComponent: (draft: ComponentsStateType) => {
  2.     const { selectedId, componentList } = draft
  3.     const index = componentList.findIndex(c => c.fe_id === selectedId)
  4.     // 如果是第一个组件,不做任何操作
  5.     if (index <= 0) return
  6.     const prevComponent = componentList[index - 1]
  7.     if (prevComponent) {
  8.       draft.selectedId = prevComponent.fe_id
  9.     }
  10.   },
  11.   selectNextComponent: (draft: ComponentsStateType) => {
  12.     const { selectedId, componentList } = draft
  13.     const index = componentList.findIndex(c => c.fe_id === selectedId)
  14.     if (index <= 0) return
  15.     if (index === componentList.length - 1) return
  16.     const nextComponent = componentList[index + 1]
  17.     if (nextComponent) {
  18.       draft.selectedId = nextComponent.fe_id
  19.     }
  20.   },
复制代码



组件库拓展操持

扩展性:


  • 从最简单的组件开始
  • 界说好规则,跑通流程
  • 增长其他组件,不改变编辑器的规则
**软件开辟规则:**对拓睁开放,对修改封闭


段落组件

需求:

要点:


  • 段落组件范例、接口、组件、属性组件实现
  • 埋伏标题:段落换行处置处罚
文件树:
  1. │   │   ├── QuestionComponents
  2. │   │   │   ├── QuestionParagraph
  3. │   │   │   │   ├── Component.tsx
  4. │   │   │   │   ├── PropComponent.tsx
  5. │   │   │   │   ├── index.ts
  6. │   │   │   │   └── interface.ts
  7. │   │   │   └── index.ts
复制代码

埋伏标题:段落换行处置处罚
只管不要使用 dangerouslySetInnerHTML 来渲染 html,会有 xss 攻击风险。
如下可以选用 map 对组件列表举行渲染
  1. const textList = text.split('\n')
  2. <Paragraph
  3.   style={{ textAlign: isCenter ? 'center' : 'start', marginBottom: 0 }}
  4. >
  5.   {/* <span dangerouslySetInnerHTML={{ __html: t }}></span> */}
  6.   {textList.map((item, index) => (
  7.     <span key={index}>
  8.       {index === 0 ? '' : <br />}
  9.       {item}
  10.     </span>
  11.   ))}
  12. </Paragraph>
复制代码
Component.tsx
  1. import React, { FC } from 'react'
  2. import {
  3.   QuestionParagraphPropsType,
  4.   QuestionParagraphDefaultProps,
  5. } from './interface'
  6. import { Typography } from 'antd'
  7. const { Paragraph } = Typography
  8. const Component: FC<QuestionParagraphPropsType> = (
  9.   props: QuestionParagraphPropsType
  10. ) => {
  11.   const { text = '', isCenter = false } = {
  12.     ...QuestionParagraphDefaultProps,
  13.     ...props,
  14.   }
  15.   // 尽量不要使用 dangerouslySetInnerHTML 来渲染 html,会有 xss 攻击风险
  16.   // const t = text.replace('\n', '<br/>')
  17.   const textList = text.split('\n')
  18.   return (
  19.     <Paragraph
  20.       style={{ textAlign: isCenter ? 'center' : 'start', marginBottom: 0 }}
  21.     >
  22.       {/* <span dangerouslySetInnerHTML={{ __html: t }}></span> */}
  23.       {textList.map((item, index) => (
  24.         <span key={index}>
  25.           {index === 0 ? '' : <br />}
  26.           {item}
  27.         </span>
  28.       ))}
  29.     </Paragraph>
  30.   )
  31. }
  32. export default Component
复制代码
index.ts
  1. /**
  2. *  @description 段落组件
  3. */
  4. import Component from './Component'
  5. import { QuestionParagraphDefaultProps } from './interface'
  6. import PropComponent from './PropComponent'
  7. export * from './interface'
  8. // paragraph 组件配置
  9. export default {
  10.   title: '段落',
  11.   type: 'questionPragraph',
  12.   Component: Component,
  13.   PropComponent: PropComponent,
  14.   defaultProps: QuestionParagraphDefaultProps,
  15. }
复制代码
interface.ts
  1. export type QuestionParagraphPropsType = {
  2.   text?: string
  3.   isCenter?: boolean
  4.   onChange?: (newProps: QuestionParagraphPropsType) => void
  5.   disabled?: boolean
  6. }
  7. export const QuestionParagraphDefaultProps: QuestionParagraphPropsType = {
  8.   text: '一行段落',
  9.   isCenter: false,
  10. }
复制代码
PropComponent.tsx
  1. import React, { FC } from 'react'
  2. import { useEffect } from 'react'
  3. import { Form, Input, Checkbox } from 'antd'
  4. import { QuestionParagraphPropsType } from './interface'
  5. const { TextArea } = Input
  6. const PropComponent: FC<QuestionParagraphPropsType> = (
  7.   props: QuestionParagraphPropsType
  8. ) => {
  9.   const { text, isCenter, onChange, disabled } = props
  10.   const [form] = Form.useForm()
  11.   useEffect(() => {
  12.     form.setFieldsValue({ text, isCenter })
  13.   }, [text, isCenter])
  14.   function handleValuesChange() {
  15.     if (onChange) {
  16.       onChange(form.getFieldsValue())
  17.     }
  18.   }
  19.   return (
  20.     <Form
  21.       layout="vertical"
  22.       initialValues={{ text, isCenter }}
  23.       form={form}
  24.       onChange={handleValuesChange}
  25.       disabled={disabled}
  26.     >
  27.       <Form.Item
  28.         label="段落内容"
  29.         name="text"
  30.         rules={[{ required: true, message: '请输入段落内容' }]}
  31.       >
  32.         <TextArea cols={5} />
  33.       </Form.Item>
  34.       <Form.Item label="是否居中" name="isCenter" valuePropName="checked">
  35.         <Checkbox />
  36.       </Form.Item>
  37.     </Form>
  38.   )
  39. }
  40. export default PropComponent
复制代码


单选框组件

   多选框同理处置处罚
  需求:

要点:


  • 组件属性面板:表单标题、默认选中、竖向编辑
  • 动态增减嵌套字段
Component.tsx
  1. import React from 'react'
  2. import { Radio, Typography } from 'antd'
  3. import { QuestionRadioPropsType, QuestionRadioDefaultProps } from './interface'
  4. const { Paragraph } = Typography
  5. const QuestionRadio: React.FC<QuestionRadioPropsType> = (
  6.   props: QuestionRadioPropsType
  7. ) => {
  8.   const { title, isVertical, options, value } = {
  9.     ...QuestionRadioDefaultProps,
  10.     ...props,
  11.   }
  12.   const radioStyle: React.CSSProperties = isVertical
  13.     ? { display: 'flex', flexDirection: 'column' }
  14.     : {}
  15.   return (
  16.     <div>
  17.       <Paragraph strong>{title}</Paragraph>
  18.       <Radio.Group
  19.         value={value}
  20.         style={radioStyle}
  21.         options={options?.map(option => ({
  22.           value: option.value,
  23.           label: option.text,
  24.         }))}
  25.       />
  26.     </div>
  27.   )
  28. }
  29. export default QuestionRadio
复制代码
PropCompnent.tsx
  1. import React, { FC } from 'react'
  2. import { useEffect } from 'react'
  3. import { Checkbox, Form, Input, Button, Space, Select } from 'antd'
  4. import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
  5. import { QuestionRadioPropsType, OptionType } from './interface'
  6. import { nanoid } from '@reduxjs/toolkit'
  7. const PropComponent: FC<QuestionRadioPropsType> = (
  8.   props: QuestionRadioPropsType
  9. ) => {
  10.   const { title, isVertical, options, value, disabled, onChange } = props
  11.   const [form] = Form.useForm()
  12.   useEffect(() => {
  13.     form.setFieldsValue({ title, isVertical, options, value })
  14.   }, [title, isVertical, options, value])
  15.   function handleValuesChange() {
  16.     const values = form.getFieldsValue()
  17.     const { options } = values
  18.     // 生成唯一的value
  19.     if (options && options.length > 0) {
  20.       options.forEach((opt: OptionType) => {
  21.         if (!opt.value) {
  22.           opt.value = nanoid(5)
  23.         }
  24.       })
  25.     }
  26.     if (onChange) {
  27.       onChange(form.getFieldsValue())
  28.     }
  29.   }
  30.   return (
  31.     <Form
  32.       layout="vertical"
  33.       initialValues={{ title, isVertical, options, value }}
  34.       onValuesChange={handleValuesChange}
  35.       form={form}
  36.       disabled={disabled}
  37.     >
  38.       <Form.Item
  39.         label="标题"
  40.         name="title"
  41.         rules={[{ required: true, message: '请输入标题' }]}
  42.       >
  43.         <Input />
  44.       </Form.Item>
  45.       <Form.Item label="选项" shouldUpdate>
  46.         <Form.List name="options">
  47.           {(fields, { add, remove }) => (
  48.             <>
  49.               {fields.map(({ key, name }) => (
  50.                 <Space key={key} align="baseline">
  51.                   <Form.Item
  52.                     name={[name, 'text']}
  53.                     rules={[
  54.                       { required: true, message: '请输入选项文字' },
  55.                       {
  56.                         validator: (_, value) => {
  57.                           const optionTexts = form
  58.                             .getFieldValue('options')
  59.                             .map((opt: OptionType) => opt.text)
  60.                           if (
  61.                             optionTexts.filter((text: string) => text === value)
  62.                               .length > 1
  63.                           ) {
  64.                             return Promise.reject(new Error('选项重复!'))
  65.                           }
  66.                           return Promise.resolve()
  67.                         },
  68.                       },
  69.                     ]}
  70.                   >
  71.                     <Input placeholder="选项文字" />
  72.                   </Form.Item>
  73.                   <MinusCircleOutlined onClick={() => remove(name)} />
  74.                 </Space>
  75.               ))}
  76.               <Form.Item>
  77.                 <Button
  78.                   type="dashed"
  79.                   onClick={() => add({ text: '', value: '' })}
  80.                   block
  81.                   icon={<PlusOutlined />}
  82.                 >
  83.                   添加选项
  84.                 </Button>
  85.               </Form.Item>
  86.             </>
  87.           )}
  88.         </Form.List>
  89.       </Form.Item>
  90.       <Form.Item label="默认选中" name="value">
  91.         <Select
  92.           options={options?.map(({ text, value }) => ({
  93.             label: text,
  94.             value: value,
  95.           }))}
  96.           allowClear
  97.           placeholder="请选择默认选项"
  98.         />
  99.       </Form.Item>
  100.       <Form.Item label="竖向排列" name="isVertical" valuePropName="checked">
  101.         <Checkbox />
  102.       </Form.Item>
  103.     </Form>
  104.   )
  105. }
  106. export default PropComponent
复制代码

表单细节:getFieldValue

在这两个文件中,getFieldValue 的使用方式差异是由于它们获取表单字段值的方式差异。
文件 PropComponent.tsx
  1. const optionTexts = form
  2. .getFieldsValue()
  3. .list.map((opt: OptionType) => opt.text)
复制代码
在这个文件中,getFieldsValue 被用来获取整个表单的全部字段值,然后通过链式调用获取 list 字段的值。list 是一个数组,此中包罗了全部选项的对象。
文件 PropComponent.tsx-1
  1. const optionTexts = form
  2. .getFieldValue('options')
  3. .map((opt: OptionType) => opt.text)
复制代码
在这个文件中,getFieldValue 被用来直接获取 options 字段的值。options 是一个数组,此中包罗了全部选项的对象。
总结


  • getFieldsValue 返回整个表单的全部字段值作为一个对象。
  • getFieldValue 必要一个参数,返回指定字段的值。
这两种方法的选择取决于你必要获取的字段值的范围。假如你只必要一个特定字段的值,使用 getFieldValue 更加直接和高效。假如你必要多个字段的值,使用 getFieldsValue 会更方便。

fix: 重复选项提示处置处罚

需求:

   解释代码运行时间,当用户添加选项,输入选项值时后,哪怕值与之前选项不重复,它会保持报“选项重复!
  1. <Form.Item
  2.   name={[name, 'text']}
  3.   rules={[
  4.     { required: true, message: '请输入选项文字' },
  5.     {
  6.       validator: (_, value) => {
  7.         const optionTexts = form
  8.           .getFieldValue('list')
  9.           .map((opt: OptionType) => opt.text)
  10.         // if (optionTexts.indexOf(value) !== -1) {
  11.         //   return Promise.reject('选项文字不能重复')
  12.         // }
  13.         if (
  14.           optionTexts.filter((text: string) => text === value)
  15.             .length > 1
  16.         ) {
  17.           return Promise.reject(new Error('选项重复!'))
  18.         }
  19.         return Promise.resolve()
  20.       },
  21.     },
  22.   ]}
  23. >
  24.   <Input />
  25. </Form.Item>
复制代码
标题缘故原由:


  • optionTexts 包罗当前正在编辑选项的旧值
  • 当用户开始输入新值时,表单立刻更新导致:
    旧值仍然存在于数组中,新值会被重复校验,纵然输入唯一值,旧值的存在也会触发校验失败
办理方案:
  1. if (
  2.   optionTexts.filter((text: string) => text === value)
  3.     .length > 1
  4. ) {
  5.   return Promise.reject(new Error('选项重复!'))
  6. }
复制代码
校验逻辑剖析

  • filter 会遍历全部选项笔墨(包罗当前正在编辑的选项)
  • 当类似笔墨出现 高出1次 时才触发错误
  • 这意味着:

    • 答应当前编辑项自身存在一次
    • 只有当其他选项存在类似笔墨时才会报错
    • 空值场景:多个空选项会触发错误(由于 "" === "")

  1. // 原错误逻辑(任意重复即报错,包含自身)
  2. if (optionTexts.indexOf(value) !== -1) { ... }
  3. // 当前逻辑(允许自身存在一次)
  4. if (重复次数 > 1) { ... }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表