newText.tsx 21 KB

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