newText.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. import TextAideInput from "@/pages/launchSystemV3/components/TextAideInput"
  2. import { extractAndFilterBracketsContent, txtLength } from "@/utils/utils"
  3. import { Button, Card, Form, InputNumber, Modal, Popconfirm, Radio, Space, Tooltip, message } from "antd"
  4. import React, { useEffect, useState } from "react"
  5. import { TextTypeList } from "../../const"
  6. import { DeleteOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"
  7. import AddTextS from "./addTextS"
  8. import style from '../index.less'
  9. import styles from '../Material/index.less'
  10. import VideoNews from "@/pages/launchSystemNew/components/newsModal/videoNews"
  11. import SelectCopyWriting from "@/pages/launchSystemV3/tencenTasset/copyWriting/selectCopyWriting"
  12. interface Props {
  13. textData: any,
  14. dynamicMaterialDTos: any,
  15. mediaType: 0 | 1 | 2 | 3,
  16. accountCreateLogs: PULLIN.AccountCreateLogsProps[]
  17. targetingLength?: number
  18. deliveryMode?: string,
  19. value?: any,
  20. visible?: boolean
  21. onClose?: () => void
  22. onChange?: (value: any) => void
  23. putInType?: 'NOVEL' | 'GAME'
  24. adDataGroup?: { [x: number]: any[] }
  25. }
  26. /**
  27. * 创意文案
  28. * @param param0
  29. * @returns
  30. */
  31. const NewText: React.FC<Props> = ({ visible, onClose, onChange, value, textData, accountCreateLogs, targetingLength = 1, dynamicMaterialDTos, mediaType, deliveryMode, putInType, adDataGroup }) => {
  32. /*************************************/
  33. const [form] = Form.useForm();
  34. const type = Form.useWatch('type', form)
  35. const textDto = Form.useWatch('textDto', form)
  36. const [textList, setTextList] = useState<PULLIN.TextDtoProps[]>([])
  37. const [desVisible, setDesVisible] = useState<boolean>(false)
  38. const [addSTitle, setAddStitle] = useState<string>()
  39. const [newText, setNewText] = useState<{ description?: string[], title?: string[] }>({})
  40. const [maxNumber, setMaxNumber] = useState<number>(1)
  41. const [groupNumber, setGroupNumber] = useState<number>(1)
  42. /*************************************/
  43. const handleOk = (values: any) => {
  44. console.log(values)
  45. const { type, textDto } = values
  46. onChange?.({
  47. type,
  48. dynamicCreativesTextDetailDTOList: textDto
  49. })
  50. }
  51. useEffect(() => {
  52. if (value && Object.keys(value).length) {
  53. const { type, dynamicCreativesTextDetailDTOList } = value
  54. form.setFieldsValue({ type, textDto: dynamicCreativesTextDetailDTOList })
  55. }
  56. }, [value])
  57. useEffect(() => {
  58. if (textData && Object.keys(textData)) {
  59. let newText: { description?: string[], title?: string[] } = {}
  60. let data = Object.values(textData).map((item: any) => {
  61. let content = item.children.content
  62. if (item.name === 'description') {
  63. newText.description = ['']
  64. } else if (item.name === 'title') {
  65. newText.title = ['']
  66. }
  67. return { label: content.description, restriction: content.restriction, value: item.name, required: item.required, arrayProperty: item?.arrayProperty }
  68. })
  69. setMaxNumber(data?.[0]?.arrayProperty?.maxNumber)
  70. setNewText(newText)
  71. if (!(value && Object.keys(value).length)) { form.setFieldsValue({ textDto: [newText] }) }
  72. setTextList(data)
  73. }
  74. }, [textData, value])
  75. // 一一对应显示素材
  76. const showMaterial = (index: number) => {
  77. const dynamicGroup = dynamicMaterialDTos?.dynamicGroup
  78. if (dynamicGroup && Object.keys(dynamicGroup).length) {
  79. let dynamic = dynamicGroup[index]
  80. const keys = Object.keys(dynamic)
  81. if (deliveryMode === "DELIVERY_MODE_CUSTOMIZE") {
  82. return <div className={style.detail_body_m}>
  83. {(keys.includes('video_id') || keys.includes('short_video1')) ? <>
  84. <div className={style.video}>
  85. <VideoNews src={dynamic?.video_id?.url || dynamic?.short_video1?.url} keyFrameImageUrl={dynamic?.video_id?.keyFrameImageUrl || dynamic?.short_video1?.keyFrameImageUrl} style={{ width: 40, height: 30 }} />
  86. {dynamic?.cover_id && <div className={style.cover_image} style={{ marginLeft: 4, width: 40, height: 30, minWidth: 42 }}>
  87. <img src={dynamic?.cover_id?.url} style={{ maxWidth: '96%', maxHeight: '96%' }} />
  88. </div>}
  89. </div>
  90. </> : keys.includes('image_id') ? <>
  91. <div className={style.cover_image} style={{ width: 40, height: 30, minWidth: 42 }}>
  92. <img src={dynamic?.image_id?.url} />
  93. </div>
  94. </> : (keys.includes('image_list') || keys.includes('element_story')) ? <>
  95. {dynamic?.[keys.includes('image_list') ? 'image_list' : 'element_story']?.map((item: { url: string | undefined; }, index: undefined) => <div className={style.cover_image} key={index} style={{ width: 30, height: 24, minWidth: 32 }}>
  96. <img src={item?.url} />
  97. </div>)}
  98. </> : null}
  99. </div>
  100. } else {
  101. return <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap' }}>
  102. {dynamic?.list?.map((item: any, index: number) => {
  103. if (Array.isArray(item)) {
  104. let length = item.length
  105. return <div className={styles.boxList_body_item} key={index} style={{ width: 30, height: 30 }}>
  106. <div className={styles.content} style={{ width: 30, height: 30 }}>
  107. {item.map((l, i) => <img src={l?.url} key={i} style={{ width: length === 6 ? 9.999 : 14.999 }} />)}
  108. </div>
  109. </div>
  110. } else if (item?.url?.includes('mp4') || item?.keyFrameImageUrl) {
  111. return <div className={styles.boxList_body_item} key={index} style={{ width: 30, height: 30 }}>
  112. <div className={styles.content} style={{ width: 30, height: 30 }}>
  113. <VideoNews src={item?.url} style={{ width: 30, height: 30 }} keyFrameImageUrl={item?.keyFrameImageUrl} maskBodyStyle={{ backgroundColor: "rgba(242, 246, 254, 0.1)" }} />
  114. </div>
  115. </div>
  116. } else {
  117. return <div className={styles.boxList_body_item} key={index} style={{ width: 30, height: 30 }}>
  118. <div className={styles.content} style={{ width: 30, height: 30 }}><img src={item?.url} /></div>
  119. </div>
  120. }
  121. })}
  122. </div>
  123. }
  124. }
  125. return null
  126. }
  127. const setText = (valList: string[]) => {
  128. const fieldData = textList.find(item => item.label === addSTitle)
  129. const count = dynamicMaterialDTos.dynamicGroup.length
  130. const valListLength = valList.length
  131. let len = 0;
  132. // 定义一个更新 textDto 的通用函数
  133. const updateTextDto = (textDto: any[]) => {
  134. return textDto.map((item: { [key: string]: any }) => {
  135. if (fieldData?.value && (item?.[fieldData.value]?.length === 0 || item?.[fieldData.value]?.length < groupNumber || !item?.[fieldData.value]?.every((t: string) => t)) && valListLength >= len + 1) {
  136. const textDto: string[] = (item?.[fieldData.value]?.length === 0 ? [''] : item?.[fieldData.value]).map((t: string) => {
  137. if (t) {
  138. return t;
  139. } else if (valListLength >= len + 1) {
  140. return valList[len++];
  141. }
  142. return t;
  143. });
  144. if (textDto.length < groupNumber && valListLength >= len + 1) {
  145. const diff = groupNumber - textDto.length;
  146. Array(diff).fill('').some(() => {
  147. if (valListLength >= len + 1) {
  148. textDto.push(valList[len++]);
  149. return false;
  150. }
  151. return true;
  152. });
  153. }
  154. return {
  155. ...item,
  156. [fieldData.value]: textDto,
  157. };
  158. }
  159. return item;
  160. });
  161. };
  162. const newTextDto = updateTextDto(textDto);
  163. if ([1, 0, 5].includes(type)) {
  164. form.setFieldsValue({
  165. textDto: newTextDto
  166. })
  167. } else if (type === 2) {
  168. let diffTextDto: any[] = []
  169. if (newTextDto.length < count && len < valListLength) {
  170. const diffCount = count - newTextDto.length
  171. const diffTextCount = Math.ceil((valListLength - len) / groupNumber)
  172. let diff = 0
  173. if (diffCount >= diffTextCount) {
  174. diff = diffTextCount
  175. } else {
  176. diff = diffCount
  177. }
  178. diffTextDto = Array(diff).fill('').map((item: { [x: string]: any }) => {
  179. if (fieldData?.value) {
  180. const textDto: string[] = []
  181. Array(groupNumber).fill('').some(() => {
  182. if (valListLength >= len + 1) {
  183. textDto.push(valList[len++])
  184. return false
  185. } else {
  186. return true
  187. }
  188. })
  189. return { ...item, [fieldData.value]: textDto }
  190. }
  191. return item
  192. })
  193. }
  194. form.setFieldsValue({
  195. textDto: [...newTextDto, ...diffTextDto]
  196. })
  197. } else if ([3, 4].includes(type)) {
  198. let diffTextDto: any[] = []
  199. if (len < valListLength) {
  200. const diff = Math.ceil((valListLength - len) / groupNumber)
  201. diffTextDto = Array(diff).fill('').map((item: { [x: string]: any }) => {
  202. if (fieldData?.value) {
  203. const textDto: string[] = []
  204. Array(groupNumber).fill('').some(() => {
  205. if (valListLength >= len + 1) {
  206. textDto.push(valList[len++])
  207. return false
  208. } else {
  209. return true
  210. }
  211. })
  212. return { ...item, [fieldData.value]: textDto }
  213. }
  214. return item
  215. })
  216. }
  217. form.setFieldsValue({
  218. textDto: [...newTextDto, ...diffTextDto]
  219. })
  220. }
  221. }
  222. return <Modal
  223. title={<Space align="center">
  224. <strong style={{ fontSize: 20 }}>创意文案</strong>
  225. {type !== 0 && <>
  226. {textList.some(item => item.value === "description") && <a style={{ fontSize: 12 }} onClick={() => { setDesVisible(true); setAddStitle(textList.find(item => item.value === "description")?.label) }}>批量添加{textList.find(item => item.value === "description")?.label}</a>}
  227. {textList.some(item => item.value === "title") && <a style={{ fontSize: 12 }} onClick={() => { setDesVisible(true); setAddStitle(textList.find(item => item.value === "description")?.label) }}>批量添加{textList.find(item => item.value === "description")?.label}</a>}
  228. </>}
  229. {textList.some(item => item.value === "description") && <>
  230. <SelectCopyWriting
  231. onClick={() => {
  232. setAddStitle(textList.find(item => item.value === "description")?.label)
  233. }}
  234. onChange={(valList) => {
  235. setText(valList)
  236. }}
  237. />
  238. <Space><span style={{ fontSize: 12 }}>每组数量:</span><InputNumber max={maxNumber} size="small" value={groupNumber} onChange={(e) => setGroupNumber(e || 1)} /></Space>
  239. </>}
  240. </Space>}
  241. open={visible}
  242. onCancel={onClose}
  243. footer={null}
  244. width={800}
  245. className={`modalResetCss`}
  246. bodyStyle={{ padding: '0 0 40px', position: 'relative', borderRadius: '0 0 8px 8px' }}
  247. maskClosable={false}
  248. >
  249. <Form
  250. form={form}
  251. name="newText"
  252. labelAlign='left'
  253. layout="vertical"
  254. colon={false}
  255. style={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: '0 10px 10px', borderRadius: '0 0 8px 8px' }}
  256. scrollToFirstError={{
  257. behavior: 'smooth',
  258. block: 'center'
  259. }}
  260. onFinishFailed={({ errorFields }) => {
  261. message.error(errorFields?.[0]?.errors?.[0])
  262. }}
  263. initialValues={{
  264. type: 0,
  265. textDto: [{ description: [''], title: [''] }]
  266. }}
  267. onFinish={handleOk}
  268. >
  269. <Card className="cardResetCss" style={{ marginTop: 10 }}>
  270. <Form.Item
  271. name="type"
  272. label={<Space>
  273. <strong>文案分配规则</strong>
  274. {type == 5 && <span style={{ fontSize: 12 }}>共<span style={{ color: 'red' }}>{dynamicMaterialDTos.dynamicGroup.length * accountCreateLogs.length}</span>个文案组</span>}
  275. </Space>}
  276. style={{ marginBottom: 0 }}
  277. rules={[{ required: true, message: '请选择文案分配规则!' }]}
  278. >
  279. <Radio.Group onChange={(e) => {
  280. let value = e.target.value
  281. let count = dynamicMaterialDTos.dynamicGroup.length
  282. let oldtextDto: PULLIN.TextDtoProps[] = JSON.parse(JSON.stringify(textDto))
  283. let length = oldtextDto.length
  284. if (value === 0 || (mediaType === 2 && value === 2)) {
  285. oldtextDto = [textDto[0] || {}]
  286. } else if (value === 1) {
  287. if (count > length) {
  288. oldtextDto = oldtextDto.concat(Array(count - length).fill(newText || { description: [''] }))
  289. } else {
  290. oldtextDto = oldtextDto.slice(0, count)
  291. }
  292. } else if (value === 2) {
  293. if (count < length) {
  294. oldtextDto = oldtextDto.slice(0, count)
  295. }
  296. } else if (value === 5) {
  297. // 创意组数量 count 广告账号数量 adLength / targetingLength 所有创意数量 count * (adLength / targetingLength)
  298. const allDynCount = count * accountCreateLogs.length
  299. if (allDynCount > length) {
  300. oldtextDto = oldtextDto.concat(Array(allDynCount - length).fill(newText || { description: [''] }))
  301. } else {
  302. oldtextDto = oldtextDto.slice(0, allDynCount)
  303. }
  304. }
  305. form.setFieldsValue({ textDto: oldtextDto })
  306. }}>
  307. {TextTypeList
  308. .filter(item => (mediaType !== 1 && mediaType !== 3) ? ![4, 5].includes(item.value) : mediaType === 3 ? true : item.value !== 5)
  309. .map(item => {
  310. if (item.value === 5 && (adDataGroup ? Object.keys(adDataGroup).some((key: any) => adDataGroup[key].length > dynamicMaterialDTos.dynamicGroup.length) : accountCreateLogs.some((pre) => ((pre?.productList || [{}]).length * targetingLength) > dynamicMaterialDTos.dynamicGroup.length))) {
  311. return <Radio value={item.value} key={item.value} disabled><Tooltip title={'账号下平均分配到广告下的创意组数量不够,不能选择此项'}>
  312. {item.label}
  313. </Tooltip></Radio>
  314. } else {
  315. return <Radio value={item.value} key={item.value}>{item.label}</Radio>
  316. }
  317. })
  318. }
  319. </Radio.Group>
  320. </Form.Item>
  321. </Card>
  322. <Form.List name="textDto">
  323. {(fields, { add, remove }) => (<>
  324. {fields.map(({ key, name, ...restField }, num) => (
  325. <Card
  326. title={type === 0 ? null : <div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
  327. {type === 1 ? <strong style={{ fontSize: 14 }}>创意组{num + 1}</strong> : type === 0 ? null : <strong style={{ fontSize: 14 }}>文案组{num + 1}</strong>}
  328. {type === 1 && showMaterial(num)}
  329. </div>}
  330. className="cardResetCss"
  331. style={{ marginTop: 10, width: '100%' }}
  332. key={key}
  333. extra={([3, 2, 4].includes(type) && textDto?.length > 1) && <Popconfirm
  334. title="你确定删除当前文案组?"
  335. onConfirm={() => remove(name)}
  336. >
  337. <DeleteOutlined style={{ color: 'red' }} />
  338. </Popconfirm>}
  339. >
  340. <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, width: '100%', flexDirection: 'column' }}>
  341. {textList.map(item => {
  342. return <Form.List
  343. {...restField}
  344. name={[name, item.value]}
  345. key={key}
  346. >
  347. {(fields, { add, remove }) => <>
  348. {fields.map((field, tIndex) => (
  349. <Form.Item
  350. {...field}
  351. label={<Space>
  352. <strong>{item.label}</strong>
  353. {textDto?.[num]?.[item.value]?.length > 1 && <a style={{ color: 'red', fontSize: 12 }} onClick={() => remove(field.name)}><MinusCircleOutlined /></a>}
  354. </Space>}
  355. style={{ marginBottom: 0, width: '100%' }}
  356. rules={[{
  357. required: item.required, validator: (rule, value) => {
  358. if (!value) {
  359. return Promise.reject('请输入正确的' + item.label)
  360. } else if (!value.match(RegExp(item.restriction.textRestriction.textPattern))) {
  361. return Promise.reject('请输入正确的' + item.label)
  362. } else if (txtLength(value) > item.restriction.textRestriction.maxLength) {
  363. return Promise.reject('请输入正确的' + item.label)
  364. }
  365. const result = extractAndFilterBracketsContent(value);
  366. if (result.extracted.length > 4) {
  367. return Promise.reject('表情数量不得超出: 4 个')
  368. }
  369. return Promise.resolve()
  370. }
  371. }]}
  372. >
  373. <TextAideInput placeholder={'请输入' + item.label} style={{ width: 580 }} maxTextLength={item.restriction.textRestriction.maxLength} putInType={putInType} />
  374. </Form.Item>
  375. ))}
  376. {deliveryMode === 'DELIVERY_MODE_COMPONENT' && item.arrayProperty?.maxNumber > 1 && textDto?.[num]?.[item.value]?.length < item.arrayProperty?.maxNumber && <Form.Item style={{ marginTop: 6, marginBottom: 0 }}>
  377. <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />} >
  378. 新增{item.label}
  379. </Button>
  380. </Form.Item>}
  381. </>}
  382. </Form.List>
  383. })}
  384. </div>
  385. </Card>
  386. ))}
  387. {[3, 2, 4].includes(type) && !(mediaType === 2 && type === 2) && !(type === 2 && textDto.length >= dynamicMaterialDTos.dynamicGroup.length) && <Form.Item style={{ marginTop: 10, marginBottom: 0 }}>
  388. <Button type="primary" onClick={() => add(newText)} block icon={<PlusOutlined />} disabled={type === 3 && textDto.length >= 30}>
  389. 新增
  390. </Button>
  391. </Form.Item>}
  392. </>)}
  393. </Form.List>
  394. <Form.Item className="submit_pull">
  395. <Space>
  396. <Button onClick={onClose}>取消</Button>
  397. <Button type="primary" htmlType="submit" className="modalResetCss">
  398. 确定
  399. </Button>
  400. </Space>
  401. </Form.Item>
  402. </Form>
  403. {/* 批量添加 */}
  404. {desVisible && <AddTextS
  405. visible={desVisible}
  406. title={addSTitle}
  407. onClose={() => {
  408. setDesVisible(false)
  409. setAddStitle(undefined)
  410. }}
  411. onChange={(value) => {
  412. if (value) {
  413. let valList = value
  414. .split(/\r?\n/) // 按换行符分割字符串
  415. .map(line => line.trim()) // 去除每行的首尾空白
  416. .filter(line => line !== ''); // 过滤掉空行
  417. setText(valList)
  418. }
  419. setDesVisible(false)
  420. setAddStitle(undefined)
  421. }}
  422. />}
  423. </Modal>
  424. }
  425. export default React.memo(NewText)