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

预期效果:

 

 功能描述:

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

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

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

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

 

实现思路:

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

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

 

具体代码:

1.父组件代码:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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.父组件样式代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.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.子组件代码顺手也贴一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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.顺便附上后端接口返回格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
    "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 @   芝麻小仙女  阅读(790)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示