addTarget.tsx 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  1. import { Button, Card, Checkbox, Form, Input, Modal, Radio, Select, Space, Spin, Tooltip, TreeSelect, Typography, message } from "antd"
  2. import React, { useEffect, useState } from "react"
  3. import style from '../index.less'
  4. import { DEVICE_PRICE_ENUM, EDUCATION_ENUM, EXCLUDED_DIMENSION_ENUM, GENDER_ENUM, LOCATION_TYPES_ENUM, MARITAL_STATUS_ENUM, NETWORK_ENUM, OPTIMIZATIONGOAL_ENUM, USER_OS_ENUM, WECHAT_AD_NEHAVIOR_ENUM } from "../../const"
  5. import { QuestionCircleFilled } from "@ant-design/icons"
  6. import { getTargetingGagsApi } from "@/services/adqV3/global"
  7. import { useAjax } from "@/Hook/useAjax"
  8. import { DefaultOptionType } from "antd/lib/select"
  9. import InputName from "@/components/InputName"
  10. import moment from "moment"
  11. import { txtLength } from "@/utils/utils"
  12. import { addTargetingApi, checkTargetingApi, updateTargetingApi } from "@/services/adqV3"
  13. import { useModel } from "umi"
  14. const { Title, Paragraph } = Typography;
  15. interface Props {
  16. isBackVal?: boolean
  17. value?: any,
  18. visible?: boolean
  19. onClose?: () => void
  20. onChange?: (targeting?: any) => void
  21. }
  22. const AddTarget: React.FC<Props> = ({ isBackVal, value, visible, onChange, onClose }) => {
  23. /******************************/
  24. const [form] = Form.useForm();
  25. const geoLocationType = Form.useWatch('geoLocationType', form);
  26. const maritalStatusType = Form.useWatch('maritalStatusType', form);
  27. const deviceBrandModelType = Form.useWatch('deviceBrandModelType', form);
  28. const wechatAdBehaviorType = Form.useWatch('wechatAdBehaviorType', form);
  29. const userOsType = Form.useWatch('userOsType', form);
  30. const ageType = Form.useWatch('ageType', form);
  31. const min = Form.useWatch(['age', 'min'], form);
  32. const max = Form.useWatch(['age', 'max'], form);
  33. const education = Form.useWatch('education', form);
  34. const networkType = Form.useWatch('networkType', form);
  35. const devicePrice = Form.useWatch('devicePrice', form);
  36. const excludedDimension = Form.useWatch(['excludedConvertedAudience', 'excludedDimension'], form);
  37. const conversionBehaviorList = Form.useWatch(['excludedConvertedAudience', 'conversionBehaviorList'], form);
  38. const actions = Form.useWatch(['wechatAdBehavior', 'actions'], form);
  39. const excludedActions = Form.useWatch(['wechatAdBehavior', 'excludedActions'], form);
  40. const [regionsList, setRegionsList] = useState<any[]>([])
  41. const [modelList, setModelList] = useState([])
  42. const [osList, setOsList] = useState<DefaultOptionType[]>([])
  43. const [isAdd, setIsAdd] = useState<boolean>(false)
  44. const getTargetingGags = useAjax((params) => getTargetingGagsApi(params))
  45. const checkTargeting = useAjax((params) => checkTargetingApi(params))
  46. const updateTargeting = useAjax((params) => updateTargetingApi(params))
  47. const addTargeting = useAjax((params) => addTargetingApi(params))
  48. const { getAllUserAccount } = useModel('useLaunchAdq.useAdAuthorize')
  49. /******************************/
  50. // 地址 手机
  51. useEffect(() => {
  52. getTargetingGags.run({ type: 'REGION' }).then(res => {
  53. let arr: any = Object.values(res).filter(v => typeof v !== 'string')
  54. let parentList = arr.filter((item: { parentName: any }) => !item.parentName)
  55. let childrenList = arr.filter((item: { parentName: any }) => item.parentName)
  56. parentList = parentList.map((item: { name: any; id: any, parentId: any }) => {
  57. let children = childrenList?.filter((c: { parentId: any }) => {
  58. return item.id === c.parentId
  59. })
  60. let obj = {
  61. title: item.name,
  62. value: item.id,
  63. key: item.id,
  64. parentId: item.parentId,
  65. disabled: item.id === 710000 || item.id === 810000 || item.id === 820000,
  66. children: children.map((item: { name: any; id: any, parentId: any }) => ({
  67. title: item.name,
  68. value: item.id,
  69. key: item.id,
  70. parentId: item.parentId,
  71. disabled: item.parentId === 710000 || item.parentId === 810000 || item.parentId === 820000
  72. }))
  73. }
  74. return obj
  75. })
  76. parentList = parentList.map((item: any) => {
  77. let itemArr = item?.children?.map((c: any) => {
  78. let arr = childrenList.filter((d: { parentId: any }) => {
  79. return d.parentId === c.value
  80. })
  81. arr = arr.map((i: { name: any; id: any }) => ({
  82. title: i.name,
  83. value: i.id,
  84. key: i.id,
  85. }))
  86. return { ...c, children: arr }
  87. })
  88. return { ...item, children: itemArr }
  89. })
  90. let zg = parentList.filter((item: { title: string }) => item.title === '中国')
  91. let zg_children = parentList.filter((item: { title: string }) => (item.title !== '中国' && item.title !== '国外'))
  92. let gw = parentList.filter((item: { title: string }) => item.title === '国外')
  93. let earth = [
  94. { ...zg[0], children: zg_children },
  95. // { ...gw[0], disabled: true }
  96. ]
  97. setRegionsList(earth)
  98. })
  99. // 获取手机
  100. getTargetingGags.run({ type: 'DEVICE_BRAND_MODEL' }).then(res => {
  101. let arr: any = Object.values(res).filter(v => typeof v !== 'string')
  102. let parentList = arr.filter((item: { parentName: any }) => !item.parentName)
  103. let childrenList = arr.filter((item: { parentName: any }) => item.parentName)
  104. parentList = parentList.map((item: { name: any; id: any }) => {
  105. let children = childrenList?.filter((c: { parentId: any }) => {
  106. return item.id === c.parentId
  107. })
  108. let obj = {
  109. title: item.name,
  110. value: item.id,
  111. key: item.id,
  112. children: children.map((item: { name: any; id: any }) => ({
  113. title: item.name,
  114. value: item.id,
  115. key: item.id,
  116. }))
  117. }
  118. return obj
  119. })
  120. setModelList(parentList)
  121. })
  122. // 获取账户列表
  123. getAllUserAccount.run()
  124. }, [])
  125. // 系统处理
  126. useEffect(() => {
  127. let iosChildren: DefaultOptionType[] = []
  128. let androidChildren: DefaultOptionType[] = []
  129. Object.keys(USER_OS_ENUM).forEach(key => {
  130. if (key !== 'ANDROID' && key !== 'IOS') {
  131. if (key.includes('ANDROID')) {
  132. androidChildren.push({
  133. label: USER_OS_ENUM[key],
  134. value: key
  135. })
  136. }
  137. if (key.includes('IOS')) {
  138. iosChildren.push({
  139. label: USER_OS_ENUM[key],
  140. value: key
  141. })
  142. }
  143. }
  144. })
  145. setOsList([{ label: 'iOS系统', value: 'IOS', children: iosChildren }, { label: 'Android系统', value: 'ANDROID', children: androidChildren }])
  146. }, [USER_OS_ENUM])
  147. const handleOk = async (values: any) => {
  148. console.log(values)
  149. const {
  150. targetingName,
  151. description,
  152. accountId,
  153. geoLocationType,
  154. ageType,
  155. geoLocation,
  156. age,
  157. gender,
  158. education,
  159. networkType,
  160. maritalStatusType,
  161. excludedConvertedAudience,
  162. deviceBrandModelType,
  163. deviceBrandModelList,
  164. isExcludedDeviceBrandModel,
  165. userOsType,
  166. os,
  167. isExcludedOs,
  168. devicePrice,
  169. wechatAdBehaviorType,
  170. ...surplusValues
  171. } = values
  172. let targetValues: any = {
  173. ...surplusValues,
  174. }
  175. if (geoLocationType === '1') {
  176. targetValues.geoLocation = {
  177. ...geoLocation,
  178. regions: geoLocation.regions[0] === 1156 ? regionsList[0]?.children?.filter((item: any) => !item.disabled)?.map((item: { value: any }) => item.value) : geoLocation.regions
  179. }
  180. }
  181. // 性别
  182. if (gender !== '0') {
  183. targetValues.gender = [gender]
  184. }
  185. // 学历
  186. if (!education?.includes('0')) {
  187. targetValues.education = education
  188. }
  189. // 联网方式
  190. if (!networkType?.includes('0')) {
  191. targetValues.networkType = networkType
  192. }
  193. // 年龄
  194. if (ageType !== '0' && ageType !== '1') {
  195. let [min, max] = ageType.split('_')
  196. targetValues.age = [{ min: Number(min), max: Number(max) }]
  197. } else if (ageType === '1') {
  198. targetValues.age = [age]
  199. }
  200. // 排除已转化用户
  201. if (excludedConvertedAudience?.excludedDimension !== '0') {
  202. targetValues.excludedConvertedAudience = excludedConvertedAudience
  203. }
  204. // 设备品牌型号 deviceBrandModelList, isExcludedDeviceBrandModel,
  205. if (deviceBrandModelType === '1') {
  206. if (isExcludedDeviceBrandModel) { // 排除
  207. targetValues.deviceBrandModel = {
  208. excludedList: deviceBrandModelList
  209. }
  210. } else {
  211. targetValues.deviceBrandModel = {
  212. includedList: deviceBrandModelList
  213. }
  214. }
  215. }
  216. // 操作系统 userOsType, os, isExcludedOs,
  217. if (userOsType === '1') {
  218. if (isExcludedOs) {
  219. targetValues.excludedOs = os
  220. } else {
  221. targetValues.userOs = os
  222. }
  223. }
  224. // 设备价格
  225. if (!devicePrice?.includes('0')) {
  226. targetValues.devicePrice = devicePrice
  227. }
  228. let targetingDTO = {
  229. targetingName,
  230. description,
  231. accountId,
  232. targeting: targetValues
  233. }
  234. if (isBackVal && !isAdd) {
  235. delete targetingDTO.accountId
  236. delete targetingDTO.description
  237. onChange?.(targetingDTO)
  238. return
  239. }
  240. let checkData = await checkTargeting.run(value?.id ? { ...targetingDTO, id: value?.id } : targetingDTO)
  241. if (checkData?.[0]?.isSame) {
  242. message.error('存在相同模板名称,请修改')
  243. return
  244. }
  245. if (value?.id) {
  246. updateTargeting.run({ ...targetingDTO, id: value?.id }).then(res => {
  247. message.success('修改成功')
  248. onChange?.()
  249. })
  250. } else {
  251. addTargeting.run(targetingDTO).then(res => {
  252. message.success('新增成功')
  253. if (isBackVal && isAdd) {
  254. delete targetingDTO.accountId
  255. delete targetingDTO.description
  256. onChange?.(targetingDTO)
  257. } else {
  258. onChange?.()
  259. }
  260. })
  261. }
  262. }
  263. // 回填数据
  264. useEffect(() => {
  265. if (value && Object.keys(value).length > 0 && regionsList.length) {
  266. let regionsAll = regionsList[0]?.children?.filter((item: any) => !item.disabled)?.map((item: { value: any }) => item.value)//全选省列表
  267. const {
  268. geoLocation,
  269. age,
  270. gender,
  271. education,
  272. networkType,
  273. maritalStatus,
  274. excludedConvertedAudience,
  275. deviceBrandModel,
  276. excludedOs,
  277. userOs,
  278. devicePrice,
  279. wechatAdBehavior,
  280. ...surplusValues
  281. } = JSON.parse(JSON.stringify(value))
  282. let targetValues: any = {
  283. ...surplusValues,
  284. age: age?.[0] || undefined,
  285. gender: gender?.[0] || '0',
  286. education: education || '0',
  287. networkType: networkType || '0',
  288. excludedConvertedAudience: excludedConvertedAudience || { excludedDimension: '0' },
  289. devicePrice: devicePrice || '0',
  290. wechatAdBehavior,
  291. maritalStatus
  292. }
  293. if (geoLocation && Object.keys(geoLocation).length > 0) {
  294. targetValues.geoLocationType = '1'
  295. targetValues.geoLocation = {
  296. ...geoLocation,
  297. regions: JSON.stringify(geoLocation.regions) === JSON.stringify(regionsAll) ? [1156] : geoLocation.regions
  298. }
  299. } else {
  300. targetValues.geoLocationType = '0'
  301. targetValues.geoLocation = {
  302. locationTypes: ['LIVE_IN']
  303. }
  304. }
  305. if (age?.length > 0) {
  306. let g = age?.[0]?.min + '_' + age?.[0]?.max
  307. if (['14_18', '19_24', '25_29', '30_39', '40_49', '50_66'].includes(g)) {
  308. targetValues.ageType = g
  309. } else {
  310. targetValues.ageType = '1'
  311. }
  312. } else {
  313. targetValues.ageType = '0'
  314. }
  315. if (maritalStatus?.length > 0) {
  316. targetValues.maritalStatusType = '1'
  317. } else {
  318. targetValues.maritalStatusType = '0'
  319. }
  320. if (deviceBrandModel?.includedList) {
  321. targetValues.deviceBrandModelType = '1'
  322. targetValues.deviceBrandModelList = deviceBrandModel?.includedList
  323. } else if (deviceBrandModel?.excludedList) {
  324. targetValues.deviceBrandModelType = '1'
  325. targetValues.deviceBrandModelList = deviceBrandModel?.excludedList
  326. targetValues.isExcludedDeviceBrandModel = true
  327. } else {
  328. targetValues.deviceBrandModelType = '0'
  329. }
  330. if (excludedOs?.length > 0) {
  331. targetValues.isExcludedOs = true
  332. targetValues.os = excludedOs
  333. targetValues.userOsType = '1'
  334. } else if (userOs?.length > 0) {
  335. targetValues.os = userOs
  336. targetValues.userOsType = '1'
  337. } else {
  338. targetValues.userOsType = '0'
  339. }
  340. let wechatAdBehaviorType = []
  341. if (wechatAdBehavior?.actions) {
  342. wechatAdBehaviorType.push('actions')
  343. }
  344. if (wechatAdBehavior?.excludedActions) {
  345. wechatAdBehaviorType.push('excludedActions')
  346. }
  347. targetValues.wechatAdBehaviorType = wechatAdBehaviorType.length > 0 ? wechatAdBehaviorType : ['0']
  348. form.setFieldsValue({ ...targetValues })
  349. }
  350. }, [value, regionsList])
  351. return <Modal
  352. title={isBackVal ? <strong style={{ fontSize: 20 }}>
  353. {(value && Object.keys(value).length > 0) ? `修改定向` : `新增定向`}
  354. </strong> : <strong style={{ fontSize: 20 }}>{value?.id ? `修改定向模板` : value?.isCopy ? `复制定向模板` : `新增定向模板`}</strong>}
  355. visible={visible}
  356. onCancel={onClose}
  357. footer={null}
  358. width={920}
  359. className={`modalResetCss`}
  360. bodyStyle={{ padding: '0 0 40px', position: 'relative', borderRadius: '0 0 8px 8px' }}
  361. maskClosable={false}
  362. >
  363. {getTargetingGags.loading && <div style={{ position: 'absolute', width: '100%', top: 0, left: 0, zIndex: 20 }}>
  364. <Spin spinning={getTargetingGags.loading}>
  365. <div style={{ width: '100%', height: 600, backgroundColor: 'rgba(255,255,255,0.95)' }}></div>
  366. </Spin>
  367. </div>}
  368. <Form
  369. form={form}
  370. name="newAdTarget"
  371. labelAlign='left'
  372. labelCol={{ span: 4 }}
  373. colon={false}
  374. style={{ backgroundColor: '#f1f4fc', maxHeight: 600, overflow: 'hidden', overflowY: 'auto', padding: '10px 10px 10px', borderRadius: '0 0 8px 8px' }}
  375. scrollToFirstError
  376. onFinishFailed={({ errorFields }) => {
  377. message.error(errorFields?.[0]?.errors?.[0])
  378. }}
  379. onFinish={handleOk}
  380. initialValues={{
  381. geoLocationType: '0',
  382. maritalStatusType: '0',
  383. deviceBrandModelType: '0',
  384. userOsType: '0',
  385. ageType: '0',
  386. wechatAdBehaviorType: ['0'],
  387. age: {
  388. min: 14,
  389. max: 66
  390. },
  391. gender: '0',
  392. education: ['0'],
  393. networkType: ['0'],
  394. devicePrice: ['0'],
  395. excludedConvertedAudience: {
  396. excludedDimension: '0'
  397. },
  398. geoLocation: {
  399. locationTypes: ['LIVE_IN']
  400. },
  401. targetingName: (isBackVal ? '定向' : '定向模板') + '_' + localStorage.getItem('userId') + '_' + moment().format('MM_DD_HH:mm:ss')
  402. }}
  403. >
  404. <Card
  405. title={<strong style={{ fontSize: 18 }}>定向选择</strong>}
  406. className="cardResetCss newCss"
  407. bodyStyle={{ padding: '4px 6px' }}
  408. style={{ marginBottom: 8 }}
  409. >
  410. <div className={style.newSpace}>
  411. <Form.Item name="geoLocationType" label={<strong>地理位置</strong>} style={{ marginBottom: 0 }}>
  412. <Radio.Group>
  413. <Radio value="0">不限</Radio>
  414. <Radio value="1">按区域</Radio>
  415. </Radio.Group>
  416. </Form.Item>
  417. {geoLocationType === '1' && <div className={style.newSpace_bottom}>
  418. {/* <Title level={5} style={{ fontSize: 14 }}>微信流量(除视频号/搜一搜)暂时仅支持 “常住地”或“旅行到访”。按法务合规要求,“常住地”暂不支持国内港澳台及国外地区,“旅行到访”仅支持部分国外地区。</Title> */}
  419. <Form.Item
  420. name={['geoLocation', 'regions']}
  421. rules={[
  422. { required: true, message: '请选择区域' },
  423. { type: 'array', max: 1000, message: '地理位置最多选择1000' },
  424. ]}
  425. >
  426. <TreeSelect
  427. placeholder="请选择"
  428. showSearch={true}
  429. maxTagCount={50}
  430. treeCheckable={true}
  431. showCheckedStrategy={TreeSelect.SHOW_PARENT}
  432. treeData={regionsList}
  433. loading={getTargetingGags.loading}
  434. style={{ width: '100%' }}
  435. allowClear
  436. filterTreeNode={(inputValue: string, treeNode: any) => {
  437. if (treeNode.title.includes(inputValue)) {
  438. return true
  439. } else {
  440. return false
  441. }
  442. }}
  443. />
  444. </Form.Item>
  445. <Form.Item
  446. name={['geoLocation', 'locationTypes']}
  447. rules={[{ required: true, message: '请选择地点类型' }]}
  448. >
  449. <Checkbox.Group
  450. disabled
  451. options={Object.keys(LOCATION_TYPES_ENUM)?.map(key => ({ label: LOCATION_TYPES_ENUM[key], value: key }))}
  452. />
  453. </Form.Item>
  454. </div>}
  455. </div>
  456. <div className={style.newSpace}>
  457. <Form.Item name="ageType" label={<strong>年龄</strong>} style={{ marginBottom: 0 }}>
  458. <Radio.Group>
  459. <Radio value="0">不限</Radio>
  460. <Radio value="14_18">14~18岁</Radio>
  461. <Radio value="19_24">19~24岁</Radio>
  462. <Radio value="25_29">25~29岁</Radio>
  463. <Radio value="30_39">30~39岁</Radio>
  464. <Radio value="40_49">40~49岁</Radio>
  465. <Radio value="50_66">50岁及以上</Radio>
  466. <Radio value="1">自定义</Radio>
  467. </Radio.Group>
  468. </Form.Item>
  469. {ageType === '1' && <div className={`${style.newSpace_bottom} flexStart`} style={{ '--g': '5px' } as React.CSSProperties}>
  470. <Form.Item name={['age', 'min']}>
  471. <Select style={{ width: 185 }} placeholder="请选择">
  472. {Array(66 - 13).fill('').map((_, i) => i + 14).filter(i => i !== 15 && i !== 16 && i !== 17).map(i => {
  473. return <Select.Option disabled={i > max} value={i} key={i}>{i === 66 ? '66 岁及以上' : i + ' 岁'}</Select.Option>
  474. })}
  475. </Select>
  476. </Form.Item>
  477. <span>-</span>
  478. <Form.Item name={['age', 'max']}>
  479. <Select style={{ width: 185 }} placeholder="请选择">
  480. {Array(66 - 17).fill('').map((_, i) => {
  481. return <Select.Option disabled={i + 18 < min} value={i + 18} key={i + 18}>{i + 18 === 66 ? '66 岁及以上' : i + 18 + ' 岁'}</Select.Option>
  482. })}
  483. </Select>
  484. </Form.Item>
  485. </div>}
  486. </div>
  487. <div className={style.newSpace}>
  488. <Form.Item name="gender" label={<strong>性别</strong>} style={{ marginBottom: 0 }}>
  489. <Radio.Group>
  490. <Radio value="0">不限</Radio>
  491. {Object.keys(GENDER_ENUM).map(key => {
  492. return <Radio value={key} key={key}>{GENDER_ENUM[key]}</Radio>
  493. })}
  494. </Radio.Group>
  495. </Form.Item>
  496. </div>
  497. <div className={style.newSpace}>
  498. <Form.Item
  499. name="education"
  500. label={<Space>
  501. <strong>学历</strong>
  502. <Tooltip title="用户的最高学历">
  503. <QuestionCircleFilled />
  504. </Tooltip>
  505. </Space>}
  506. style={{ marginBottom: 0 }}
  507. getValueFromEvent={(e: string[]) => {
  508. if (e.length > 1 && !education.includes('0') && e.includes('0')) {
  509. return ['0'];
  510. }
  511. return e.length > 0 ? (e.length > 1 && e.includes('0') ? e.filter(item => item !== '0') : e) : ['0'];
  512. }}
  513. >
  514. <Checkbox.Group
  515. options={[
  516. { label: '不限', value: '0' },
  517. ...Object.keys(EDUCATION_ENUM)?.map(key => ({ label: EDUCATION_ENUM[key], value: key }))
  518. ]}
  519. />
  520. </Form.Item>
  521. </div>
  522. <div className={style.newSpace}>
  523. <Form.Item
  524. name="networkType"
  525. label={<strong>联网方式</strong>}
  526. style={{ marginBottom: 0 }}
  527. getValueFromEvent={(e: string[]) => {
  528. if (e.length > 1 && !networkType.includes('0') && e.includes('0')) {
  529. return ['0'];
  530. }
  531. return e.length > 0 ? (e.length > 1 && e.includes('0') ? e.filter(item => item !== '0') : e) : ['0'];
  532. }}
  533. >
  534. <Checkbox.Group
  535. options={[
  536. { label: '不限', value: '0' },
  537. ...Object.keys(NETWORK_ENUM)?.map(key => ({ label: NETWORK_ENUM[key], value: key }))
  538. ]}
  539. />
  540. </Form.Item>
  541. </div>
  542. <div className={style.newSpace}>
  543. <Form.Item
  544. label={<Space>
  545. <strong>自定义人群</strong>
  546. <Tooltip title={<span>
  547. 自定义人群是指客户通过腾讯广告知数(原DMP)创建和管理自己定义类人群,包括您自行上传的号码包人群等。仅当出价方式选择CPC、 CPM或oCPM(且优化目标为“点击”)时,你可以“二方人群”进行投放。
  548. <a href="https://e.qq.com/ads/helpcenter/detail/?cid=3161&pid=8995" target="__blank">了解更多</a>
  549. </span>}>
  550. <QuestionCircleFilled />
  551. </Tooltip>
  552. </Space>}
  553. style={{ marginBottom: 0 }}
  554. >
  555. <Space>
  556. <Checkbox.Group
  557. defaultValue={['0']}
  558. options={[
  559. { label: '不限', value: '0' }
  560. ]}
  561. />
  562. <span style={{ color: '#FAAD14' }}>自定义人群必须关联指定账户,当前关联账户为不限</span>
  563. </Space>
  564. </Form.Item>
  565. </div>
  566. <div className={style.newSpace}>
  567. <Form.Item name="maritalStatusType" label={<strong>婚恋育儿状态</strong>} style={{ marginBottom: maritalStatusType === '1' ? 4 : 12 }}>
  568. <Radio.Group>
  569. <Radio value="0">不限</Radio>
  570. <Radio value="1">自定义</Radio>
  571. </Radio.Group>
  572. </Form.Item>
  573. {maritalStatusType === '1' && <div className={`${style.newSpace_bottom}`} style={{ marginBottom: 6 }}>
  574. <Form.Item name={'maritalStatus'} rules={[{ required: true, message: '请选择婚恋育儿状态' }]}>
  575. <Checkbox.Group options={Object.keys(MARITAL_STATUS_ENUM).map(key => ({ label: MARITAL_STATUS_ENUM[key], value: key }))} />
  576. </Form.Item>
  577. </div>}
  578. <Form.Item
  579. name={['excludedConvertedAudience', 'excludedDimension']}
  580. label={<Space>
  581. <strong>排除已转化用户</strong>
  582. <Tooltip title={<>
  583. <Paragraph>
  584. 设置排除已转化定向,广告不会曝光给所选范围内已转化的用户;
  585. 系统将自动以当前广告选择的优化目标作为此定向的转化行为(不支持“点击”、“次留率”优化目标);
  586. 该定向仅可选择oCPC、oCPM、oCPA出价方式,若勾选自定义转化行为则不限制出价方式 。
  587. </Paragraph>
  588. <a href="https://e.qq.com/ads/helpcenter/detail/?cid=3531&pid=2612" target="__blank">了解更多</a>
  589. </>}>
  590. <QuestionCircleFilled />
  591. </Tooltip>
  592. </Space>}
  593. style={{ marginBottom: excludedDimension === '0' ? 12 : 4 }}
  594. >
  595. <Radio.Group>
  596. <Radio value="0">不限</Radio>
  597. {Object.keys(EXCLUDED_DIMENSION_ENUM).map(key => {
  598. return <Radio value={key} key={key}>{EXCLUDED_DIMENSION_ENUM[key]}</Radio>
  599. })}
  600. </Radio.Group>
  601. </Form.Item>
  602. {excludedDimension !== '0' && <div className={`${style.newSpace_bottom}`} style={{ marginBottom: 6 }}>
  603. <Title level={5} style={{ fontSize: 14 }}>系统自动依照当前广告选择的优化目标作为此定向的转化行为</Title>
  604. <Form.Item label="自定义转化行为" name={['excludedConvertedAudience', 'conversionBehaviorList']} rules={[{ required: true, message: '请选择自定义转化行为' }]}>
  605. <Select
  606. showSearch
  607. filterOption={(input, option) =>
  608. (option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
  609. }
  610. allowClear
  611. placeholder='请选择自定义转化行为'
  612. mode="multiple"
  613. style={{ width: 480 }}
  614. >
  615. {Object.keys(OPTIMIZATIONGOAL_ENUM).filter(key => key !== 'OPTIMIZATIONGOAL_NONE').map(key => {
  616. return <Select.Option value={key} key={key} disabled={conversionBehaviorList?.length >= 2 && !conversionBehaviorList.includes(key)}>{OPTIMIZATIONGOAL_ENUM[key]}</Select.Option>
  617. })}
  618. </Select>
  619. </Form.Item>
  620. </div>}
  621. <Form.Item name="deviceBrandModelType" label={<strong>设备品牌型号</strong>} style={{ marginBottom: deviceBrandModelType === '0' ? 0 : 4 }}>
  622. <Radio.Group>
  623. <Radio value="0">不限</Radio>
  624. <Radio value="1">自定义</Radio>
  625. </Radio.Group>
  626. </Form.Item>
  627. {deviceBrandModelType === '1' && <div className={`${style.newSpace_bottom}`} style={{ marginBottom: 6 }}>
  628. <Form.Item
  629. name={'deviceBrandModelList'}
  630. rules={[
  631. { required: true, message: '请选择设备品牌' },
  632. { type: 'array', max: 400, message: '设备品牌最多选择400个设备型号' }
  633. ]}
  634. style={{ marginBottom: 10 }}
  635. >
  636. <TreeSelect
  637. placeholder="请选择"
  638. showSearch={true}
  639. maxTagCount={20}
  640. treeCheckable={true}
  641. showCheckedStrategy={TreeSelect.SHOW_CHILD}
  642. treeData={modelList}
  643. style={{ width: '100%' }}
  644. allowClear
  645. filterTreeNode={(inputValue: string, treeNode: any) => {
  646. if (treeNode.title.includes(inputValue)) {
  647. return true
  648. } else {
  649. return false
  650. }
  651. }}
  652. />
  653. </Form.Item>
  654. <Form.Item name='isExcludedDeviceBrandModel' valuePropName="checked">
  655. <Checkbox>排除所选设备的用户</Checkbox>
  656. </Form.Item>
  657. </div>}
  658. </div>
  659. <div className={style.newSpace}>
  660. <Form.Item name="userOsType" label={<strong>操作系统版本</strong>} style={{ marginBottom: 0 }}>
  661. <Radio.Group>
  662. <Radio value="0">不限</Radio>
  663. <Radio value="1">自定义</Radio>
  664. </Radio.Group>
  665. </Form.Item>
  666. {userOsType === '1' && <div className={`${style.newSpace_bottom}`}>
  667. <Form.Item
  668. name={'os'}
  669. style={{ marginBottom: 10 }}
  670. rules={[
  671. { required: true, message: '请选择操作系统版本' },
  672. { type: 'array', max: 100, message: '操作系统版本最多选择100个设备型号' }
  673. ]}
  674. >
  675. <TreeSelect
  676. placeholder="请选择"
  677. showSearch={true}
  678. maxTagCount={10}
  679. treeCheckable={true}
  680. showCheckedStrategy={TreeSelect.SHOW_PARENT}
  681. treeData={osList}
  682. style={{ width: '100%' }}
  683. allowClear
  684. filterTreeNode={(inputValue: string, treeNode: any) => {
  685. if (treeNode.title.includes(inputValue)) {
  686. return true
  687. } else {
  688. return false
  689. }
  690. }}
  691. />
  692. </Form.Item>
  693. <Form.Item name='isExcludedOs' valuePropName="checked">
  694. <Checkbox>排除所选操作系统版本的用户</Checkbox>
  695. </Form.Item>
  696. </div>}
  697. </div>
  698. <div className={style.newSpace}>
  699. <Form.Item
  700. name="devicePrice"
  701. label={<strong>设备价格</strong>}
  702. style={{ marginBottom: 0 }}
  703. getValueFromEvent={(e: string[]) => {
  704. if (e.length > 1 && !devicePrice.includes('0') && e.includes('0')) {
  705. return ['0'];
  706. }
  707. return e.length > 0 ? (e.length > 1 && e.includes('0') ? e.filter(item => item !== '0') : e) : ['0'];
  708. }}
  709. >
  710. <Checkbox.Group
  711. options={[
  712. { label: '不限', value: '0' },
  713. ...Object.keys(DEVICE_PRICE_ENUM)?.map(key => ({ label: DEVICE_PRICE_ENUM[key], value: key }))
  714. ]}
  715. />
  716. </Form.Item>
  717. </div>
  718. <div className={style.newSpace}>
  719. <Form.Item
  720. name="wechatAdBehaviorType"
  721. label={<strong>微信再营销</strong>}
  722. style={{ marginBottom: 0 }}
  723. getValueFromEvent={(e: string[]) => {
  724. if (e.length > 1 && !wechatAdBehaviorType.includes('0') && e.includes('0')) {
  725. return ['0'];
  726. }
  727. return e.length > 0 ? (e.length > 1 && e.includes('0') ? e.filter(item => item !== '0') : e) : ['0'];
  728. }}
  729. >
  730. <Checkbox.Group
  731. options={[
  732. { label: '不限', value: '0' },
  733. { label: '再营销', value: 'actions' },
  734. { label: '排除营销', value: 'excludedActions' }
  735. ]}
  736. />
  737. </Form.Item>
  738. {(wechatAdBehaviorType && !wechatAdBehaviorType?.includes('0')) && <div className={`${style.newSpace_bottom}`}>
  739. {wechatAdBehaviorType.includes('actions') && <>
  740. <Title level={5} style={{ fontSize: 14 }}>再营销</Title>
  741. <Form.Item style={{ marginBottom: 10 }} name={['wechatAdBehavior', 'actions']}>
  742. <Checkbox.Group options={Object.keys(WECHAT_AD_NEHAVIOR_ENUM).filter(item => item !== 'WECHAT_WORK_CONTACTS_ADDED').map(key => ({ label: WECHAT_AD_NEHAVIOR_ENUM[key], value: key, disabled: excludedActions?.some((k: string) => k === key) }))} />
  743. </Form.Item>
  744. </>}
  745. {wechatAdBehaviorType.includes('excludedActions') && <>
  746. <Title level={5} style={{ fontSize: 14 }}>排除营销</Title>
  747. <Form.Item name={['wechatAdBehavior', 'excludedActions']}>
  748. <Checkbox.Group options={Object.keys(WECHAT_AD_NEHAVIOR_ENUM).map(key => ({ label: WECHAT_AD_NEHAVIOR_ENUM[key], value: key, disabled: actions?.some((k: string) => k === key) }))} />
  749. </Form.Item>
  750. </>}
  751. </div>}
  752. </div>
  753. </Card>
  754. <Card
  755. title={<strong style={{ fontSize: 18 }}>定向设置</strong>}
  756. className="cardResetCss"
  757. >
  758. <Form.Item
  759. label={<strong>定向模板名称</strong>}
  760. name='targetingName'
  761. // tooltip="下标、日期时分秒、广告账户创建时默认自带"
  762. rules={[
  763. { required: true, message: '请输入定向模板名称!' },
  764. {
  765. required: true, message: '定向模板名称不能包含特殊字符:< > & ‘ ” / 以及TAB、换行、回车键,请修改', validator(_, value) {
  766. let reg = /[&‘’“”/\n\t\f]/ig
  767. if (value && reg.test(value)) {
  768. return Promise.reject()
  769. }
  770. return Promise.resolve()
  771. }
  772. },
  773. {
  774. required: true, message: '请确保定向模板名称长度不超过30个字(1个汉字等于2个字符)', validator(_, value) {
  775. if (value && txtLength(value) > 30) {
  776. return Promise.reject()
  777. }
  778. return Promise.resolve()
  779. }
  780. }
  781. ]}
  782. >
  783. <InputName placeholder='定向模板名称' style={{ width: 480 }} length={30} />
  784. </Form.Item>
  785. {!isBackVal && <>
  786. <Form.Item
  787. label={<strong>定向模板描述</strong>}
  788. name='description'
  789. >
  790. <Input.TextArea style={{ width: 480 }} placeholder="定向模板描述" />
  791. </Form.Item>
  792. <Form.Item
  793. label={<strong>关联账户</strong>}
  794. name='accountId'
  795. >
  796. <Select
  797. style={{ width: 480 }}
  798. showSearch
  799. filterOption={(input, option) =>
  800. (option!.children as unknown as string)?.toLowerCase()?.includes(input?.toLowerCase())
  801. }
  802. allowClear
  803. placeholder='请选择媒体账户'
  804. >
  805. {getAllUserAccount?.data?.data?.map((item: any) => <Select.Option value={item.accountId} key={item.id}>{item.remark ? item.accountId + '_' + item.remark : item.accountId}</Select.Option>)}
  806. </Select>
  807. </Form.Item>
  808. </>}
  809. </Card>
  810. <Form.Item className="submit_pull">
  811. <Space>
  812. {isBackVal && !(value && Object.keys(value).length > 0) && <Checkbox checked={isAdd} onChange={(e) => setIsAdd(e.target.checked)}>是否保存到定向模板</Checkbox>}
  813. <Button onClick={onClose}>取消</Button>
  814. <Button type="primary" htmlType="submit" className="modalResetCss" loading={checkTargeting.loading || updateTargeting.loading || addTargeting.loading}>
  815. 确定
  816. </Button>
  817. </Space>
  818. </Form.Item>
  819. </Form>
  820. </Modal>
  821. }
  822. export default React.memo(AddTarget)