copyPage.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import InputName from "@/components/InputName"
  2. import { useAjax } from "@/Hook/useAjax"
  3. import { getImagesInfoApi, getVideosInfoApi } from "@/services/adqV3/global"
  4. import { txtLength } from "@/utils/utils"
  5. import { DeleteOutlined, PlusOutlined } from "@ant-design/icons"
  6. import { Button, Card, Form, message, Modal, Space, Spin } from "antd"
  7. import React, { useEffect, useState } from "react"
  8. import styles from '../../tencentAdPutIn/create/Material/index.less'
  9. import VideoNews from "@/pages/launchSystemNew/components/newsModal/videoNews"
  10. import { useModel } from "umi"
  11. import SelectCloud from "@/pages/launchSystemNew/components/selectCloud"
  12. import { batchCreateDownPageApi } from "@/services/adqV3"
  13. interface Props {
  14. accountId: number
  15. pageData: any
  16. visible?: boolean
  17. onClose?: () => void
  18. onChange?: () => void
  19. }
  20. /**
  21. * 批量复制落地页
  22. * @returns
  23. */
  24. const CopyPage: React.FC<Props> = ({ accountId, pageData, visible, onChange, onClose }) => {
  25. /********************************/
  26. const { init } = useModel('useLaunchAdq.useBdMediaPup')
  27. const [materialConfig, setMaterialConfig] = useState<{
  28. adcreativeTemplateId?: number,
  29. type: string,
  30. cloudSize: { relation: string, width: number, height: number }[],
  31. list: any[],
  32. index: number,
  33. max: number,
  34. sliderImgContent: any,
  35. isGroup?: boolean
  36. }>({
  37. type: '',//类型
  38. cloudSize: [],//素材搜索条件
  39. list: [],//素材
  40. index: 0, // 素材组下标
  41. max: 1,//素材数量
  42. sliderImgContent: undefined
  43. })//图片素材配置
  44. const [selectVideoVisible, setSelectVideoVisible] = useState(false)
  45. const [form] = Form.useForm();
  46. const downPageMaterialDTOS = Form.useWatch('downPageMaterialDTOS', form)
  47. const [mType, setMtype] = useState<{ width: number, height: number, type: "IMAGE" | 'VIDEO' | 'CAROUSEL', number: number }>()
  48. const getImagesInfo = useAjax((params) => getImagesInfoApi(params))
  49. const getVideosInfo = useAjax((params) => getVideosInfoApi(params))
  50. const batchCreateDownPage = useAjax((params) => batchCreateDownPageApi(params))
  51. /********************************/
  52. useEffect(() => {
  53. console.log(pageData)
  54. if (pageData?.pageElementsSpecList) {
  55. form.setFieldsValue({ pageName: pageData?.pageName })
  56. let { elementType, ...spec } = pageData?.pageElementsSpecList[0]
  57. switch (elementType) {
  58. case "IMAGE": // 图片
  59. case "CAROUSEL": // 轮播
  60. let imageIdList = spec.imageSpec.imageIdList
  61. getImagesInfo.run({ adAccountId: accountId, imageIds: [imageIdList[0]], pageNum: 1, pageSize: 1 }).then(res => {
  62. if (res?.records?.length) {
  63. const { imageWidth, imageHeight } = res.records[0]
  64. setMtype({ width: imageWidth, height: imageHeight, type: elementType, number: imageIdList.length })
  65. }
  66. })
  67. break
  68. case "VIDEO": // 视频
  69. let videoId = spec.videoSpec.videoId
  70. getVideosInfo.run({ adAccountId: accountId, videoIds: [videoId] }).then(res => {
  71. console.log(res)
  72. if (res?.length) {
  73. const { width, height } = res[0]
  74. setMtype({ width, height, type: elementType, number: 1 })
  75. }
  76. })
  77. break
  78. }
  79. }
  80. }, [pageData])
  81. const handleOk = (values: any) => {
  82. console.log(values)
  83. const { pageName, downPageMaterialDTOS: data } = values
  84. let downPageMaterialDTOS: { materialType: number, videoId?: number, imageId?: number[] }[] = []
  85. if (mType?.type === 'VIDEO') {
  86. downPageMaterialDTOS = data.map((item: { videoId: { id: any; materialType: any } }) => {
  87. const { id, materialType } = item.videoId
  88. return { videoId: id, materialType }
  89. })
  90. } else {
  91. downPageMaterialDTOS = data.map((item: { imageId: { id: any; materialType: any }[] }) => {
  92. const { materialType } = item.imageId[0]
  93. return { imageId: item.imageId.map(item => item.id), materialType }
  94. })
  95. }
  96. batchCreateDownPage.run({ pageName, downPageMaterialDTOS, accountId, templateId: pageData.pageId }).then(res => {
  97. if (res?.data?.length) {
  98. message.error(res.data.toString())
  99. } else {
  100. message.success('新增成功')
  101. onChange?.()
  102. }
  103. })
  104. }
  105. return <Modal
  106. title={<Space>
  107. <strong>批量替换顶部素材复制落地页</strong>
  108. {(mType?.type && ['CAROUSEL', 'IMAGE'].includes(mType?.type)) && <Button type="link" onClick={() => {
  109. init({ mediaType: 'IMG', num: 100, cloudSize: [[{ relation: '=', width: mType.width, height: mType.height }]], maxSize: 307200 })
  110. setMaterialConfig({
  111. ...materialConfig,
  112. type: 'imageId',
  113. max: mType.number || 1,
  114. index: 99999,
  115. adcreativeTemplateId: 721
  116. })
  117. setTimeout(() => {
  118. setSelectVideoVisible(true)
  119. }, 100)
  120. }}>批量添加图片素材</Button>}
  121. {(mType?.type && ['VIDEO'].includes(mType?.type)) && <Button type="link" onClick={() => {
  122. init({ mediaType: 'VIDEO', num: 100, cloudSize: [[{ relation: '=', width: mType.width, height: mType.height }]], maxSize: 20971520 })
  123. setMaterialConfig({
  124. ...materialConfig,
  125. type: 'videoId',
  126. max: 1,
  127. index: 99999,
  128. adcreativeTemplateId: 311
  129. })
  130. setTimeout(() => {
  131. setSelectVideoVisible(true)
  132. }, 100)
  133. }}>批量添加视频素材</Button>}
  134. </Space>}
  135. visible={visible}
  136. onCancel={onClose}
  137. className="modalResetCss"
  138. width={900}
  139. bodyStyle={{ padding: '0 0 40px', position: 'relative', borderRadius: '0 0 8px 8px' }}
  140. footer={null}
  141. >
  142. <Form
  143. form={form}
  144. name="copyPage"
  145. labelAlign='left'
  146. layout="vertical"
  147. colon={false}
  148. style={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: 10, borderRadius: '0 0 8px 8px' }}
  149. scrollToFirstError={{
  150. behavior: 'smooth',
  151. block: 'center'
  152. }}
  153. onFinishFailed={({ errorFields }) => {
  154. message.error(errorFields?.[0]?.errors?.[0])
  155. }}
  156. onFinish={handleOk}
  157. initialValues={{
  158. downPageMaterialDTOS: [undefined]
  159. }}
  160. >
  161. <Card className="cardResetCss" style={{ marginBottom: 10 }}>
  162. <Form.Item
  163. label={<strong>落地页名称</strong>}
  164. name='pageName'
  165. rules={[
  166. { required: true, message: '请输入落地页名称!' },
  167. {
  168. required: true, message: '广告名称不能包含特殊字符:< > & ‘ ” / 以及TAB、换行、回车键,请修改', validator(_, value) {
  169. let reg = /[&‘’“”/\n\t\f]/ig
  170. if (value && reg.test(value)) {
  171. return Promise.reject()
  172. }
  173. return Promise.resolve()
  174. }
  175. },
  176. {
  177. required: true, message: '请确保落地页名称长度不超过40个字', validator(_, value) {
  178. if (value && txtLength(value) > 40) {
  179. return Promise.reject()
  180. }
  181. return Promise.resolve()
  182. }
  183. }
  184. ]}
  185. >
  186. <InputName placeholder='落地页名称' style={{ width: 680 }} length={40} />
  187. </Form.Item>
  188. </Card>
  189. {mType ? <Form.List name="downPageMaterialDTOS">
  190. {(fields, { add, remove }) => (<>
  191. <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
  192. {fields.map((field, num) => (<Card
  193. title={<strong style={{ fontSize: 18 }}>素材组{num + 1}</strong>}
  194. className="cardResetCss"
  195. key={field.key}
  196. style={{ width: mType?.type === 'CAROUSEL' ? '100%' : downPageMaterialDTOS?.length > 1 ? 'calc(50% - 5px)' : '100%' }}
  197. extra={fields?.length > 1 && <DeleteOutlined className={styles.clear} onClick={() => remove(field.name)} style={{ color: 'red' }} />}
  198. >
  199. <Space size={30} style={{ width: '100%' }} className={styles.space}>
  200. {mType?.type === "VIDEO" ? <Form.Item
  201. {...field}
  202. label={<strong>视频</strong>}
  203. rules={[{ required: true, message: '请选择素材!' }]}
  204. name={[field.name, 'videoId']}
  205. >
  206. <div className={`${styles.box} ${styles.video}`} style={{ width: 300, height: 160 }} onClick={() => {
  207. init({ mediaType: 'VIDEO', cloudSize: [[{ relation: '=', width: mType.width, height: mType.height }]], maxSize: 20971520 })
  208. setMaterialConfig({
  209. ...materialConfig,
  210. type: 'videoId',
  211. max: 1,
  212. index: num,
  213. adcreativeTemplateId: 311
  214. })
  215. setTimeout(() => {
  216. setSelectVideoVisible(true)
  217. }, 100)
  218. }}>
  219. <div className={styles.p}>
  220. {downPageMaterialDTOS?.length > 0 && downPageMaterialDTOS[num] && Object.keys(downPageMaterialDTOS[num])?.includes('videoId') ? <VideoNews src={downPageMaterialDTOS[num]['videoId']['url']} style={{ display: 'block', width: 'auto', margin: 0, height: '100%' }} maskImgStyle={{ position: 'absolute', top: '50%', left: '50%', width: 40, height: 40, transform: 'translate(-50%, -50%)', zIndex: 10 }} /> : <>
  221. <span>{`推荐尺寸(${mType?.width} x ${mType?.height})`}</span>
  222. <span>{`${['MEDIA_TYPE_MP4', 'MEDIA_TYPE_MOV', 'MEDIA_TYPE_AVI'].map((str: any) => str?.replace('MEDIA_TYPE_', ''))};< ${20}M;时长 ≥ ${5}s,≤ ${300}s,必须带有声音`}</span>
  223. </>}
  224. </div>
  225. </div>
  226. </Form.Item> : mType?.type === 'IMAGE' ? <Form.Item
  227. {...field}
  228. label={<strong>图片</strong>}
  229. rules={[{ required: true, message: '请选择素材!' }]}
  230. name={[field.name, 'imageId']}
  231. >
  232. <Space align="end">
  233. <div className={`${styles.box} ${styles.image}`} style={{ width: 300, height: 160 }} onClick={() => {
  234. init({ mediaType: 'IMG', cloudSize: [[{ relation: '=', width: mType.width, height: mType.height }]], maxSize: 307200 })
  235. setMaterialConfig({
  236. ...materialConfig,
  237. type: 'imageId',
  238. max: 1,
  239. index: num,
  240. adcreativeTemplateId: 721
  241. })
  242. setTimeout(() => {
  243. setSelectVideoVisible(true)
  244. }, 100)
  245. }}>
  246. <p>
  247. {downPageMaterialDTOS?.length > 0 && downPageMaterialDTOS[num]?.imageId?.length ? <img src={downPageMaterialDTOS[num]['imageId'][0]['url']} /> : <>
  248. <span>{`推荐尺寸(${mType?.width} x ${mType?.height})`}</span>
  249. <span>{`${['IMAGE_TYPE_JPG', 'IMAGE_TYPE_PNG'].map((str: any) => str?.replace('IMAGE_TYPE_', ''))};小于 300KB`}</span>
  250. </>}
  251. </p>
  252. </div>
  253. </Space>
  254. </Form.Item> : mType?.type === 'CAROUSEL' ? <Form.Item
  255. {...field}
  256. label={<strong>轮播图</strong>}
  257. rules={[
  258. { required: true, type: 'array', len: mType.number, message: '素材数量不对!' },
  259. { required: true, message: '请选择素材!' },
  260. ]}
  261. name={[field.name, 'imageId']}
  262. >
  263. <div className={`${styles.box} ${mType.number >= 3 ? styles.image_list : styles.imageMater}`} style={mType.number >= 3 ? { flexFlow: 'row', width: '100%', gap: 2 } : {}} onClick={() => {
  264. init({ mediaType: 'IMG', num: mType.number, cloudSize: [[{ relation: '=', width: mType.width, height: mType.height }]], maxSize: 307200 })
  265. setMaterialConfig({
  266. ...materialConfig,
  267. type: 'imageId',
  268. max: mType.number,
  269. index: num,
  270. adcreativeTemplateId: 721
  271. })
  272. setTimeout(() => {
  273. setSelectVideoVisible(true)
  274. }, 100)
  275. }}>
  276. {Array(mType.number).fill('').map((arr, index1) => {
  277. return <p key={index1} style={mType.number >= 3 ? { width: 130, height: 130 } : { justifyContent: 'center' }}>
  278. {downPageMaterialDTOS?.length > 0 && downPageMaterialDTOS[num] && Object.keys(downPageMaterialDTOS[num])?.includes('imageId') && downPageMaterialDTOS[num]['imageId'][index1] ? <img src={downPageMaterialDTOS[num]['imageId'][index1]['url']} /> : <>
  279. <span>{`推荐尺寸(${mType?.width} x ${mType?.height})`}</span>
  280. <span>{`${['IMAGE_TYPE_JPG', 'IMAGE_TYPE_PNG']?.map((str: any) => str?.replace('IMAGE_TYPE_', ''))};小于 ${300}KB`}</span>
  281. </>}
  282. </p>
  283. })}
  284. </div>
  285. </Form.Item> : null}
  286. </Space>
  287. </Card>))}
  288. </div>
  289. <Form.Item style={{ marginBottom: 0, marginTop: 10 }}>
  290. <Button type="dashed" style={{ color: '#1890ff' }} onClick={() => add()} block icon={<PlusOutlined />}>添加素材组</Button>
  291. </Form.Item>
  292. </>)}
  293. </Form.List> : <Spin spinning={getImagesInfo.loading}><div style={{ width: '100%', height: 100 }}></div></Spin>}
  294. <Form.Item className="submit_pull">
  295. <Space>
  296. <Button onClick={onClose}>取消</Button>
  297. <Button type="primary" htmlType="submit" className="modalResetCss" loading={batchCreateDownPage.loading}>
  298. 确定
  299. </Button>
  300. </Space>
  301. </Form.Item>
  302. </Form>
  303. {selectVideoVisible && <SelectCloud
  304. isGroup={materialConfig?.isGroup}
  305. visible={selectVideoVisible}
  306. onClose={() => setSelectVideoVisible(false)}
  307. sliderImgContent={materialConfig.index === 99999 ? undefined :
  308. materialConfig.type === 'imageId' ? downPageMaterialDTOS[materialConfig.index]?.imageId?.length ? downPageMaterialDTOS[materialConfig.index]['imageId']?.map((item: any) => item) : undefined :
  309. (downPageMaterialDTOS[materialConfig.index] && Object.keys(downPageMaterialDTOS[materialConfig.index])?.includes(materialConfig.type)) ? [{ url: downPageMaterialDTOS[materialConfig.index][materialConfig.type] }] : undefined
  310. }
  311. onChange={(content: any) => {
  312. if (content.length > 0) {
  313. console.log(content)
  314. if (materialConfig.index === 99999) {
  315. if (materialConfig.type === 'imageId') {
  316. let urls = content?.map((item: any) => ({ id: item?.id, url: item?.url, materialType: 0 }))
  317. let max = materialConfig.max
  318. let materialsNew = downPageMaterialDTOS.map((item: any) => {
  319. let newItem = item || {}
  320. // 判断是否有字段,是否设置了值
  321. if (Object.keys(newItem).includes(materialConfig.type) && newItem[materialConfig.type]) {
  322. if (max > newItem[materialConfig.type].length && urls.length > 0) {
  323. let difference = max - newItem[materialConfig.type].length
  324. let material: any[] = []
  325. if (urls.length >= difference) {
  326. material = urls.splice(0, difference)
  327. } else {
  328. material = urls.splice(0, urls.length)
  329. }
  330. newItem[materialConfig.type] = [...newItem[materialConfig.type], ...material]
  331. return newItem
  332. } else {
  333. return newItem
  334. }
  335. } else {
  336. if (urls.length >= max) {
  337. let material = urls.splice(0, max)
  338. return { ...newItem, [materialConfig.type]: material }
  339. } else if (urls.length > 0) {
  340. let material = urls.splice(0, urls.length)
  341. return { ...newItem, [materialConfig.type]: material }
  342. } else {
  343. return newItem
  344. }
  345. }
  346. })
  347. if (urls.length > 0) {
  348. let data = Array(Math.ceil(urls.length / max)).fill(undefined).map(item => {
  349. if (urls.length >= max) {
  350. let material = urls.splice(0, max)
  351. return { [materialConfig.type]: material }
  352. } else {
  353. let material = urls.splice(0, urls.length)
  354. return { [materialConfig.type]: material }
  355. }
  356. })
  357. materialsNew = [...materialsNew, ...data]
  358. }
  359. console.log('materialsNew-->', materialsNew)
  360. form.setFieldsValue({ downPageMaterialDTOS: materialsNew })
  361. } else {
  362. let newMaterials = content?.map((item: any) => {
  363. return { [materialConfig.type]: { url: item?.url, id: item?.id, materialType: 0 } }
  364. })
  365. if (newMaterials.length > 0) {
  366. if (downPageMaterialDTOS?.every((item: any) => !item)) { // 没设置过
  367. form.setFieldsValue({ downPageMaterialDTOS: newMaterials })
  368. } else { // 设置过
  369. let materialsNew = downPageMaterialDTOS.map((item: any) => {
  370. let newItem = item || {}
  371. if (Object.keys(newItem).includes(materialConfig.type) && newItem[materialConfig.type]) {
  372. return item
  373. } else {
  374. if (newMaterials.length > 0) {
  375. let material = newMaterials.splice(0, 1)
  376. return { ...newItem, ...material[0] }
  377. } else {
  378. return item
  379. }
  380. }
  381. })
  382. if (newMaterials.length > 0) {
  383. materialsNew = [...materialsNew, ...newMaterials]
  384. }
  385. form.setFieldsValue({ downPageMaterialDTOS: materialsNew })
  386. }
  387. }
  388. }
  389. } else {
  390. let newDynamicGroup = downPageMaterialDTOS?.map((item: any, index: number) => {
  391. if (materialConfig.index === index) {
  392. if (materialConfig.type === 'imageId') {
  393. if (item) {
  394. item[materialConfig.type] = content?.map((item: any) => ({ id: item?.id, url: item?.url, materialType: 0 }))
  395. return { ...item }
  396. } else {
  397. return { [materialConfig.type]: content?.map((item: any) => ({ id: item?.id, url: item?.url, materialType: 0 })) }
  398. }
  399. } else {
  400. if (item) {
  401. item[materialConfig.type] = { id: content[0]?.id, url: content[0]?.url, materialType: 0 }
  402. return { ...item }
  403. } else {
  404. return { [materialConfig.type]: { id: content[0]?.id, url: content[0]?.url, materialType: 0 } }
  405. }
  406. }
  407. }
  408. return item
  409. })
  410. form.setFieldsValue({ downPageMaterialDTOS: newDynamicGroup })
  411. }
  412. }
  413. setSelectVideoVisible(false)
  414. }}
  415. />}
  416. </Modal>
  417. }
  418. export default React.memo(CopyPage)