• 【React+antd】做一个动态增减文案组的组件


    预期效果:

     功能描述:

    1.初始化只展示一个按钮,通过按钮添加新的组,可删除,可清空

    2.每个文案组都是独立的块,具体里面有几个文案,根据后端动态返回的内容进行渲染

    3.可以选择已有标题列表中的标题,赋值到输入框中

    4.内部有自己的校验,输入框赋值后也应触发校验,其中每个文案可能存在是否非必填、最大长度、最小长度的校验,以及文案格式的正则校验

    实现思路:

    1.组件参考antd表单文档中提供的【动态增减表单项】的代码进行实现(https://ant.design/components/form-cn/#components-form-demo-dynamic-form-item)

    2.子组件设计为抽屉,由父组件的按钮触发

    具体代码:

    1.父组件代码:

    import React, { useState } from 'react';
    import { Button, Form, Input, Card, Row, Col, message } from 'antd';
    import CopyTitle from './CopyTitle';
    
    export interface Props {
      id?: string;
      value?: Record<string, any>[];
      defaultValue?: Record<string, any>[];
      onChange?: (values: Record<string, any>[]) => void;
      require?: boolean;
      placeholder?: string; // 输入提示
      maxLength?: string; //
      columns?: API.FormListType[];
      formRef?: any;
    }
    
    /**
     * 文案组组件
     */
    const CreativeCopywriting: React.FC<Props> = (props) => {
      const inputRef = React.useRef<any>(null);
      const { id, onChange, value, columns, formRef } = props;
      const [visible, setVisible] = useState<boolean>(false);
      const [titleMaxLength, setTitleMaxLength] = useState<number>();
      const [titleMinLength, setTitleMinLength] = useState<number>();
      const [copyName, setCopyName] = useState<number | string>();
      const [copyId, setCopyId] = useState<string>();
    
      // 选择已有标题-打开抽屉
      const handleCopy = (formItem: API.FormListType, name: number | string, formItemId: string) => {
        setTitleMaxLength(formItem?.formItemProps?.maxLength);
        setTitleMinLength(formItem?.formItemProps?.minLength);
        setCopyName(name);
        setCopyId(formItemId);
        setVisible(true);
      };
    
      // 确认选择标题
      const onCopy = (title: string) => {
        const newValues = value?.map((item: any, index: number) => {
          if (index === copyName) {
            const valuesObj = { ...item };
            valuesObj[`${copyId}`] = title;
            return valuesObj;
          }
          return item;
        });
        formRef?.current?.setFieldsValue({ text_group: newValues });
        formRef?.current?.validateFields(['text_group']);
        if (onChange) onChange(newValues || []);
      };
    
      const handleClear = (name: number | string) => {
        const valuesObj = {};
        columns?.forEach((item: API.FormListType) => {
          valuesObj[`${item.id}`] = '';
        });
        const newValues = value?.map((item: any, index: number) => {
          if (index === name) {
            return valuesObj;
          }
          return item;
        });
    
        if (onChange) onChange(newValues || []);
      };
    
      return (
        <>
          <Form.List
            name={id || 'text_group'}
            rules={[
              {
                validator: async () => {
                  return Promise.resolve();
                },
              },
            ]}
          >
            {(fields, { add, remove }) => (
              <>
                <Button
                  type="primary"
                  onClick={() => {
                    if (fields && fields.length < 5) {
                      add();
                    } else {
                      message.error('文案组最多5条');
                    }
                  }}
                >
                  添加文案组
                </Button>
                {fields.map(({ key, name, ...restField }) => (
                  <Card
                    bodyStyle={{ padding: '8px' }}
                    style={{ margin: '8px 0 0' }}
                    key={`${id}_${key}_${name}`}
                  >
                    <div
                      style={{
                        margin: '0 0 8px',
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'flex-end',
                      }}
                    >
                      <a
                        onClick={() => remove(name)}
                        style={{ display: 'inline-block', marginRight: '16px' }}
                        key={`${id}_${key}_${name}_delete`}
                      >
                        删除
                      </a>
                      <a onClick={() => handleClear(name)} key={`${id}_${key}_${name}_clear`}>
                        清空
                      </a>
                    </div>
                    {columns &&
                      columns.length &&
                      columns.map((item: API.FormListType, index: number) => {
                        return (
                          <Row key={`${id}_${key}_${name}_${index}_Row`}>
                            <Col
                              span={4}
                              style={{
                                height: '32px',
                                display: 'flex',
                                alignItems: 'center',
                                justifyContent: 'flex-end',
                                paddingRight: '8px',
                              }}
                            >
                              {item.label}
                            </Col>
                            <Col span={14}>
                              <Form.Item
                                {...restField}
                                key={`${id}_${key}_${name}_${index}_${item.id}`}
                                name={[name, `${item.id}`]}
                                validateTrigger={['onChange', 'onBlur', 'onInput']}
                                rules={[
                                  {
                                    validator: (_, values) => {
                                      const { pattern } = item?.fieldProps?.rules[0];
                                      if (item.required && !values) {
                                        return Promise.reject(new Error(`请输入${item.label}`));
                                      }
                                      if (pattern) {
                                        const newReg = new RegExp(pattern);
                                        if (values && !newReg.test(values)) {
                                          return Promise.reject(
                                            new Error(item?.fieldProps?.rules[0].message),
                                          );
                                        }
                                      }
                                      if (
                                        values &&
                                        values.length &&
                                        item?.formItemProps?.minLength &&
                                        values.length < item?.formItemProps?.minLength
                                      ) {
                                        return Promise.reject(
                                          new Error(`长度不能少于${item?.formItemProps?.minLength}个字`),
                                        );
                                      }
                                      if (
                                        values &&
                                        values.length &&
                                        item?.formItemProps?.maxLength &&
                                        values.length > item?.formItemProps?.maxLength
                                      ) {
                                        return Promise.reject(
                                          new Error(`长度不能超过${item?.formItemProps?.maxLength}个字`),
                                        );
                                      }
                                      return Promise.resolve();
                                    },
                                  },
                                ]}
                              >
                                <Input placeholder="请输入" ref={inputRef} id={`${name}${item.id}`} />
                              </Form.Item>
                            </Col>
                            <Col span={4}>
                              <Button
                                style={{ marginLeft: '16px' }}
                                type="default"
                                onClick={() => {if (item.id) handleCopy(item, name, item.id);}}
                                key={`${id}_${key}_${name}_${index}_${item.id}_copy`}
                              >
                                选择已有标题
                              </Button>
                            </Col>
                          </Row>
                        );
                      })}
                  </Card>
                ))}
              </>
            )}
          </Form.List>
          <CopyTitle
            key={`copyDrawer`}
            visible={visible}
            onSubmit={onCopy}
            onClose={() => { setVisible(false)}}
            maxLength={titleMaxLength}
            minLength={titleMinLength}
          />
        </>
      );
    };
    
    export default CreativeCopywriting;
    

      

    2.父组件样式代码:

    .c-base-tag {
      &-form {
        height: 440px;
        overflow-y: scroll;
      }
      &-result {
        height: 72px;
        overflow-y: scroll;
      }
    
      &-form,
      &-result {
        &::-webkit-scrollbar {
           8px;
        }
    
        &::-webkit-scrollbar-thumb {
          background: #cfd1d5;
          border-radius: 10px;
        }
    
        &::-webkit-scrollbar-track-piece {
          background: transparent;
        }
      }
    }
    

      

    3.子组件代码顺手也贴一下:

    import React, { useEffect, useState } from 'react';
    import { Drawer, Button, message, Space, Spin } from 'antd';
    import { useRequest } from 'umi';
    import type { ProColumns } from '@ant-design/pro-table';
    import type { ParamsType } from '@ant-design/pro-provider';
    import TableList from '@/components/TableList';
    import type { PaginationProps } from 'antd';
    import { wxTitleInit, wxTitleList } from '../services';
    
    export interface Props {
      visible: boolean;
      onSubmit?: (values: string) => void;
      onClose?: () => void;
      maxLength?: number;
      minLength?: number;
    }
    
    const CopyTitle: React.FC<Props> = (props) => {
      const { visible, onSubmit, onClose, maxLength, minLength = 1 } = props;
      const [searchData, setSearchData] = useState<API.FormListType[]>([]);
      const [tableData, setTableData] = useState<any[]>([]);
      const [tableColumns, setTableColumns] = useState<ProColumns[]>([]);
      const [tablePage, setTablePage] = useState<PaginationProps>({});
      const [tableParams, setTableParams] = useState<ParamsType>({});
      const [selectedList, setSelectedList] = useState<any[]>([]); // 已选择
    
      // 获取页面初使化数据
      const { loading: pageLoading, run: init } = useRequest(() => wxTitleInit(), {
        manual: true,
        onSuccess: (result) => {
          // 初使化数据赋值
          const { searchList = [], pageDefault = {} } = result || {};
          setSearchData(searchList);
          // 初使化完成后获取列表数据
          if (pageDefault) setTableParams(pageDefault);
        },
      });
    
      const { loading: tableLoading, run: runTable } = useRequest(
        () => wxTitleList({ ...tableParams, minLength, maxLength, channelId: ['default', 'weixin'] }),
        {
          manual: true,
          onSuccess: (result) => {
            if (result) {
              setTableColumns([]);
              setTablePage({});
              const { tableHeaderList = [], tableList = [], page } = result;
              setTableData(tableList);
              setTableColumns([
                ...tableHeaderList.map((el) => {
                  if (el.dataIndex === 'title') {
                    return { ...el,  200 };
                  }
                  if (el.dataIndex === 'game') {
                    return { ...el,  100 };
                  }
                  if (el.dataIndex === 'channel') {
                    return { ...el,  50 };
                  }
                  if (el.dataIndex === 'update_time') {
                    return { ...el,  100 };
                  }
                  return el;
                }),
              ]);
              if (page) setTablePage(page);
            }
          },
        },
      );
    
      useEffect(() => {
        if (visible && tableParams) {
          setSelectedList([]);
          runTable();
        }
      }, [tableParams, visible]);
    
      // 根据渠道获取页面初使化数据
      useEffect(() => {
        setTableData([]);
        init();
      }, []);
    
      return (
        <Drawer
          width={800}
          visible={visible}
          title={`选择已有标题`}
          destroyOnClose
          footer={
            <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
              <Space>
                <Button
                  onClick={() => {
                    if (onClose) onClose();
                    setSelectedList([]);
                  }}
                >
                  取 消
                </Button>
                <Button
                  type="primary"
                  onClick={() => {
                    if (selectedList.length === 0) {
                      message.error(`至少选择一条标题`);
                    } else {
                      if (onSubmit) onSubmit(selectedList[0].title || '');
                      if (onClose) onClose();
                    }
                  }}
                >
                  确 定
                </Button>
              </Space>
            </div>
          }
          onClose={() => {
            if (onClose) onClose();
            setSelectedList([]);
          }}
        >
          <Spin spinning={pageLoading}>
            <TableList
              loading={tableLoading}
              columns={tableColumns}
              dataSource={tableData}
              pagination={tablePage}
              search={searchData}
              tableAlertRender={false}
              toolBarRender={false}
              rowSelection={{
                alwaysShowAlert: false,
                type: 'radio',
                onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
                  setSelectedList(selectedRows);
                },
              }}
              onChange={(params) => setTableParams(params)}
            />
          </Spin>
        </Drawer>
      );
    };
    
    export default CopyTitle;

      

    4.顺便附上后端接口返回格式:

    {
        "id": "text_group",
        "label": "文案组",
        "type": "textGroup",
        "required": true,
        "fieldProps": {
            "columns": [
                {
                    "id": "title",
                    "label": "标题",
                    "type": "text",
                    "required": true,
                    "formItemProps": {
                        "minLength": 1,
                        "maxLength": 12
                    },
                    "fieldProps": {
                        "rules": [
                            {
                                "pattern": "^[^\\<\\>\\&'\\\"\\/\\x08\\x09\\x0A\\x0D\\\\]+$",
                                "message": "请输入正确标题"
                            }
                        ]
                    }
                },
                {
                    "id": "description",
                    "label": "首行文案",
                    "type": "text",
                    "required": true,
                    "formItemProps": {
                        "minLength": 1,
                        "maxLength": 16
                    },
                    "fieldProps": {
                        "rules": [
                            {
                                "pattern": "^[^\\<\\>\\&'\\\"\\/\\x08\\x09\\x0A\\x0D\\\\]+$",
                                "message": "请输入正确首行文案"
                            }
                        ]
                    }
                },
                {
                    "id": "caption",
                    "label": "次行文案",
                    "type": "text",
                    "required": true,
                    "formItemProps": {
                        "minLength": 1,
                        "maxLength": 16
                    },
                    "fieldProps": {
                        "rules": [
                            {
                                "pattern": "^[^\\<\\>\\&'\\\"\\/\\x08\\x09\\x0A\\x0D\\\\]+$",
                                "message": "请输入正确次行文案"
                            }
                        ]
                    }
                },
                {
                    "id": "left_bottom_txt",
                    "label": "第三行文案",
                    "type": "text",
                    "required": false,
                    "formItemProps": {
                        "minLength": 1,
                        "maxLength": 16
                    },
                    "fieldProps": {
                        "rules": [
                            {
                                "pattern": "^[^\\<\\>\\&'\\\"\\/\\x08\\x09\\x0A\\x0D\\\\]+$",
                                "message": "请输入正确第三行文案"
                            }
                        ]
                    }
                }
            ]
        }
    }
    

      

  • 相关阅读:
    古代规模最大的战争:长平之战(做事不能太小气,不同的将领有不同的视角,要智胜,活着很重要)
    聚集索引更新后会不会马上重新排序
    GitHub Pages 搭建流程-基于jekyll-bootstrap
    OpenStack调研
    领域模型设计
    Load ContextCLR 探测
    Sql Server Job 简单使用
    Power Designer导出实体类和NHibernate xml文件
    解决跨域
    性能计数器
  • 原文地址:https://www.cnblogs.com/nangras/p/15993491.html
Copyright © 2020-2023  润新知