addTarget.tsx 47 KB

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