【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 {
      width: 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, width: 200 };
              }
              if (el.dataIndex === 'game') {
                return { ...el, width: 100 };
              }
              if (el.dataIndex === 'channel') {
                return { ...el, width: 50 };
              }
              if (el.dataIndex === 'update_time') {
                return { ...el, width: 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": "请输入正确第三行文案"
                        }
                    ]
                }
            }
        ]
    }
}

  

 

posted @ 2022-03-11 14:08  芝麻小仙女  阅读(716)  评论(1编辑  收藏  举报