adgroupsPrice.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import { Card, Form, Input, InputNumber, Space, Switch, Tooltip } from "antd"
  2. import React, { useContext, useEffect, useState } from "react"
  3. import { DispatchAd } from "./newCreateAd";
  4. import New1Radio from "@/pages/launchSystemV3/components/New1Radio";
  5. import { BID_ALL_OCATION_MODE, BID_MODE_ENUM, BID_SCENE_NORMAL_ENUM, OPTIMIZATIONGOAL_ENUM, ROI_ALL_OCATION_MODE, SMART_BID_TYPE_ENUM } from "../../const";
  6. import { QuestionCircleFilled } from "@ant-design/icons";
  7. import { toCamelCase } from "@/utils/utils";
  8. import style from '../index.less'
  9. /**
  10. * 出价与预算
  11. * @returns
  12. */
  13. const AdgroupsPrice: React.FC = () => {
  14. /****************************************/
  15. const { form, setOGPparams, OGPParams, putInType, smartDeliveryGoalRules } = useContext(DispatchAd)!;
  16. const [smartDeliveryGoalSpecRules, setSmartDeliveryGoalSpecRules] = useState<PULLIN.SmartDeliveryGoalSpecProps[]>([])
  17. const [smartDeliveryGoalSpecName, setSmartDeliveryGoalSpecName] = useState<string>('spec')
  18. const siteSet = Form.useWatch('siteSet', form)
  19. const bidMode = Form.useWatch('bidMode', form)
  20. const bidAllocationMode = Form.useWatch('bidAllocationMode', form)
  21. const roiAllocationMode = Form.useWatch(['deepConversionSpec', 'deepConversionWorthSpec', 'roiAllocationMode'], form)
  22. const optimizationGoal = Form.useWatch('optimizationGoal', form)
  23. const smartBidType = Form.useWatch('smartBidType', form)
  24. const bidScene = Form.useWatch('bidScene', form)
  25. const autoAcquisitionEnabled = Form.useWatch('autoAcquisitionEnabled', form)
  26. const automaticSiteEnabled = Form.useWatch('automaticSiteEnabled', form)
  27. const deepConversionType = Form.useWatch(['deepConversionSpec', 'deepConversionType'], form);
  28. const deliveryMethod = Form.useWatch('deliveryMethod', form);
  29. const smartDeliverySceneSpec = Form.useWatch('smartDeliverySceneSpec', form);
  30. const goal = Form.useWatch(['deepConversionSpec', deepConversionType === 'DEEP_CONVERSION_BEHAVIOR' ? 'deepConversionBehaviorSpec' : 'deepConversionWorthSpec', 'goal'], form);
  31. /****************************************/
  32. useEffect(() => {
  33. if (deliveryMethod === 'SMART' && smartDeliveryGoalRules?.length && smartDeliverySceneSpec?.smartDeliveryGoal) {
  34. const rule = smartDeliveryGoalRules.find(item => item.value === smartDeliverySceneSpec.smartDeliveryGoal)
  35. const smartDeliveryGoalSpecRules = rule?.parameter_definitions?.smart_delivery_goal_spec
  36. setSmartDeliveryGoalSpecRules(smartDeliveryGoalSpecRules || [])
  37. const smartDeliveryGoalSpecName = toCamelCase(rule?.smart_delivery_goal_spec_name || "")
  38. setSmartDeliveryGoalSpecName(smartDeliveryGoalSpecName)
  39. }
  40. }, [smartDeliveryGoalRules, smartDeliverySceneSpec, deliveryMethod])
  41. return <Card
  42. title={<strong style={{ fontSize: 18 }}>出价与预算</strong>}
  43. className="cardResetCss"
  44. >
  45. <Form.Item
  46. label={<Space>
  47. <strong>计费方式</strong>
  48. <Tooltip title={`设置了优化目标CPM、CPC、CPA不可选`}>
  49. <QuestionCircleFilled />
  50. </Tooltip>
  51. </Space>}
  52. name='bidMode'
  53. rules={[{ required: true, message: '请选择计费方式' }]}
  54. >
  55. <New1Radio
  56. data={Object.keys(BID_MODE_ENUM).filter(key => {
  57. if (deliveryMethod === 'SMART') return key === 'BID_MODE_OCPM';
  58. if (siteSet?.some((name: string) => ['SITE_SET_CHANNELS', 'SITE_SET_MOMENTS'].includes(name)) || automaticSiteEnabled) {
  59. return key === 'BID_MODE_OCPM' || key === 'BID_MODE_CPM'
  60. } else {
  61. return true
  62. }
  63. })?.map(key => ({ label: BID_MODE_ENUM[key as keyof typeof BID_MODE_ENUM], value: key, disabled: optimizationGoal && ['BID_MODE_CPM', 'BID_MODE_CPC', 'BID_MODE_CPA'].includes(key) ? true : false }))}
  64. onChange={(e) => {
  65. // form.setFieldsValue({ siteSet: defaultSiteSet })
  66. setOGPparams({ ...OGPParams, automaticSiteEnabled: e, bidMode: e as string })
  67. if (e === "BID_MODE_CPM" || e === "BID_MODE_CPC") {
  68. form.setFieldsValue({
  69. optimizationGoal: null,
  70. smartBidType: null,
  71. bidScene: null,
  72. // bidAmount:null,
  73. bidStrategy: null,
  74. autoAcquisitionEnabled: false,
  75. autoAcquisitionBudget: null,
  76. dailyBudget: null,
  77. })
  78. } else {
  79. form.setFieldsValue({
  80. optimizationGoal: "OPTIMIZATIONGOAL_ECOMMERCE_ORDER",
  81. smartBidType: "SMART_BID_TYPE_CUSTOM",
  82. bidScene: "BID_SCENE_NORMAL_AVERAGE",
  83. bidAmount: '1000',
  84. bidStrategy: "BID_STRATEGY_TARGET_COST",
  85. autoAcquisitionEnabled: false,
  86. autoAcquisitionBudget: null,
  87. dailyBudget: null,
  88. })
  89. }
  90. }}
  91. />
  92. </Form.Item>
  93. {deliveryMethod === 'NORMAL' ? <>
  94. {/* 常规3.0 */}
  95. {(bidMode === 'BID_MODE_OCPM' || bidMode === 'BID_MODE_OCPC') && <>
  96. {putInType === 'GAME' ? <Form.Item label={<strong>出价场景</strong>} name='bidScene' rules={[{ required: true, message: '请选择出价场景' }]}>
  97. <New1Radio data={Object.keys(BID_SCENE_NORMAL_ENUM).map(key => ({ label: BID_SCENE_NORMAL_ENUM[key as keyof typeof BID_SCENE_NORMAL_ENUM], value: key }))} />
  98. </Form.Item> : <Form.Item label={<strong>出价类型</strong>} name='smartBidType' rules={[{ required: true, message: '请选择出价类型' }]}>
  99. <New1Radio data={Object.keys(SMART_BID_TYPE_ENUM).map(key => ({ label: SMART_BID_TYPE_ENUM[key as keyof typeof SMART_BID_TYPE_ENUM], value: key }))} />
  100. </Form.Item>}
  101. </>}
  102. {(putInType === 'GAME' ? bidScene !== 'BID_SCENE_NORMAL_MAX' : smartBidType !== 'SMART_BID_TYPE_SYSTEMATIC') && <>
  103. <Form.Item label={<strong>出价分配方式</strong>} name='bidAllocationMode' rules={[{ required: true, message: '请选择出价分配方式' }]}>
  104. <New1Radio data={BID_ALL_OCATION_MODE} />
  105. </Form.Item>
  106. {bidAllocationMode === 1 ? <Form.Item label={<strong>出价</strong>} name='bidAmount' rules={[{ required: true, message: '请输入价格' }]}>
  107. <Input
  108. placeholder={`请输入价格`}
  109. style={{ width: 480 }}
  110. suffix={`元/${optimizationGoal ? OPTIMIZATIONGOAL_ENUM[optimizationGoal as keyof typeof OPTIMIZATIONGOAL_ENUM] : ['BID_MODE_OCPM', 'BID_MODE_OCPC'].includes(bidMode) ? '千次曝光' : '点击'}`}
  111. />
  112. </Form.Item> : <Form.Item label={<strong>{bidAllocationMode === 2 ? '随机出价' : '阶梯出价'}</strong>} required>
  113. <Space>
  114. <Form.Item name='bidAmountMin' rules={[{ required: true, message: '请输入价格最小值' }]} noStyle>
  115. <Input
  116. placeholder={`请输入价格最小值`}
  117. style={{ width: 200 }}
  118. />
  119. </Form.Item>
  120. <span>-</span>
  121. <Form.Item name='bidAmountMax' rules={[{ required: true, message: '请输入价格最大值' }]} noStyle>
  122. <Input
  123. placeholder={`请输入价格最大值`}
  124. style={{ width: 200 }}
  125. />
  126. </Form.Item>
  127. <span>元/{optimizationGoal ? OPTIMIZATIONGOAL_ENUM[optimizationGoal as keyof typeof OPTIMIZATIONGOAL_ENUM] : ['BID_MODE_OCPM', 'BID_MODE_OCPC'].includes(bidMode) ? '千次曝光' : '点击'}</span>
  128. </Space>
  129. </Form.Item>}
  130. {deepConversionType === 'DEEP_CONVERSION_BEHAVIOR' ? <>
  131. <Form.Item label={<strong>深度目标出价</strong>} name={['deepConversionSpec', 'deepConversionBehaviorSpec', 'bidAmount']} rules={[{ required: true, message: '请输入深度目标出价' }]}>
  132. <Input style={{ width: 480 }} suffix={`元/${OPTIMIZATIONGOAL_ENUM[goal as keyof typeof OPTIMIZATIONGOAL_ENUM] || '优化目标'}`} placeholder={`请输入深度目标出价,范围0.1~10000`} />
  133. </Form.Item>
  134. </> :
  135. deepConversionType === 'DEEP_CONVERSION_WORTH' ? <>
  136. <Form.Item label={<strong>ROI分配方式</strong>} name={['deepConversionSpec', 'deepConversionWorthSpec', 'roiAllocationMode']} rules={[{ required: true, message: '请选择ROI分配方式' }]}>
  137. <New1Radio data={ROI_ALL_OCATION_MODE} />
  138. </Form.Item>
  139. {roiAllocationMode === 1 ? <Form.Item
  140. label={<strong>期望ROI</strong>}
  141. name={['deepConversionSpec', 'deepConversionWorthSpec', 'expectedRoi']}
  142. rules={[
  143. { required: true, message: '请输入期望ROI' },
  144. { type: 'number', ...(goal === 'GOAL_1DAY_MONETIZATION_ROAS' ? { min: 0.001, max: 50, message: '范围0.001~50' } : { min: 0.001, max: 1000, message: '范围0.001~1000' }) },
  145. {
  146. validator: (_: any, value: string) => {
  147. if (!value || /^\d+(\.\d{0,3})?$/.test(value)) {
  148. return Promise.resolve();
  149. }
  150. return Promise.reject(new Error('请输入最多三位小数'));
  151. }
  152. }
  153. ]}
  154. >
  155. <InputNumber style={{ width: 480 }} placeholder={`期望ROI目标范围${goal === 'GOAL_1DAY_MONETIZATION_ROAS' ? '0.001~50' : '0.001~1000'},输入0.05,表示ROI目标为5%`} />
  156. </Form.Item> : <Form.Item label={<strong>{roiAllocationMode === 2 ? '随机ROI' : '阶梯ROI'}</strong>} required>
  157. <Space>
  158. <Form.Item
  159. name={['deepConversionSpec', 'deepConversionWorthSpec', 'expectedRoiMin']}
  160. rules={[
  161. { required: true, message: '请输入期望ROI最小值' },
  162. { type: 'number', ...(goal === 'GOAL_1DAY_MONETIZATION_ROAS' ? { min: 0.001, max: 50, message: '范围0.001~50' } : { min: 0.001, max: 1000, message: '范围0.001~1000' }) },
  163. {
  164. validator: (_: any, value: string) => {
  165. if (!value || /^\d+(\.\d{0,3})?$/.test(value)) {
  166. return Promise.resolve();
  167. }
  168. return Promise.reject(new Error('请输入最多三位小数'));
  169. }
  170. }
  171. ]}
  172. noStyle
  173. >
  174. <InputNumber
  175. placeholder={`请输入期望ROI最小值`}
  176. style={{ width: 228 }}
  177. />
  178. </Form.Item>
  179. <span>-</span>
  180. <Form.Item
  181. name={['deepConversionSpec', 'deepConversionWorthSpec', 'expectedRoiMax']}
  182. rules={[
  183. { required: true, message: '请输入期望ROI最大值' },
  184. { type: 'number', ...(goal === 'GOAL_1DAY_MONETIZATION_ROAS' ? { min: 0.001, max: 50, message: '范围0.001~50' } : { min: 0.001, max: 1000, message: '范围0.001~1000' }) },
  185. {
  186. validator: (_: any, value: string) => {
  187. if (!value || /^\d+(\.\d{0,3})?$/.test(value)) {
  188. return Promise.resolve();
  189. }
  190. return Promise.reject(new Error('请输入最多三位小数'));
  191. }
  192. }
  193. ]}
  194. noStyle
  195. >
  196. <InputNumber
  197. placeholder={`请输入期望ROI最大值`}
  198. style={{ width: 228 }}
  199. />
  200. </Form.Item>
  201. </Space>
  202. </Form.Item>}
  203. </> : null}
  204. {optimizationGoal === 'OPTIMIZATIONGOAL_24H_FIRSTPAY' && <Form.Item
  205. style={{ marginBottom: 10 }}
  206. label={<Space>
  207. <strong>一方数据跑量加强</strong>
  208. <Tooltip title={<div>
  209. <p>基于您规范回传的一方数据,系统将其作为补充样本针对性优化OCPX模型,并助力提升广告投放拿量能力</p>
  210. </div>}>
  211. <QuestionCircleFilled />
  212. </Tooltip>
  213. </Space>}
  214. name='ecomPkamSwitch'
  215. valuePropName="checked"
  216. help="注意账号需要开通权限"
  217. >
  218. <Switch checkedChildren="开启" unCheckedChildren="关闭" />
  219. </Form.Item>}
  220. {((bidMode === 'BID_MODE_OCPM' || bidMode === 'BID_MODE_OCPC') && (putInType === 'GAME' ? bidScene !== 'BID_SCENE_NORMAL_MAX' : smartBidType !== 'SMART_BID_TYPE_SYSTEMATIC')) && <>
  221. <Form.Item
  222. style={{ marginBottom: 10 }}
  223. label={<Space>
  224. <strong>一键起量</strong>
  225. <Tooltip title={<div>
  226. <p>1. 一键起量原理:给该广告提供一笔起量预算,系统会在 6 小时内快速花完预算,帮助广告激进探索,获取更多曝光,期间转化成本可能高于预期;</p>
  227. <p>
  228. <span>2. 一键起量注意事项:</span><br />
  229. 探索中任何原因导致广告暂停播放,都会导致起量中止,且恢复播放后也不会再继续探索; 一键起量期间产生的消耗不赔付,但转化计入赔付门槛判断;你可以在该广告的一键起量状态中止或结束时,重新设置起量预算,开始一次新的起量周期
  230. </p>
  231. <p>
  232. <span>点击查看</span><a href="https://e.qq.com/ads/helpcenter/detail?cid=3532&pid=2004" target="__blank">赔付规则</a><br />
  233. <span>点击了解</span><a href="https://e.qq.com/ads/helpcenter/detail?cid=3532&pid=2005" target="__blank">一键起量</a>
  234. </p>
  235. </div>}>
  236. <QuestionCircleFilled />
  237. </Tooltip>
  238. </Space>}
  239. name='autoAcquisitionEnabled'
  240. valuePropName="checked"
  241. >
  242. <Switch checkedChildren="开启" unCheckedChildren="关闭" />
  243. </Form.Item>
  244. {/* 一键起量开启时才出现 */}
  245. {autoAcquisitionEnabled && <Form.Item
  246. name='autoAcquisitionBudget'
  247. rules={[{ required: true, message: '请输入起量预算' }]}
  248. help={<div>
  249. <span>1. 一键起量期间产生的消耗不赔付,但转化计入赔付门槛判断</span><br />
  250. <span>2. 一键起量可能导致转化成本高于预期,且起量结束后不一定能持续消耗</span>
  251. </div>}
  252. >
  253. <Input placeholder='请输入起量预算,建议设置为出价的10倍,范围 200~100000 元,不能低于出价' style={{ width: 560 }} suffix="元" />
  254. </Form.Item>}
  255. </>}
  256. </>}
  257. </> : <>
  258. {/* 智投 */}
  259. <Form.Item label={<strong>出价分配方式</strong>} name='bidAllocationMode' rules={[{ required: true, message: '请选择出价分配方式' }]}>
  260. <New1Radio data={BID_ALL_OCATION_MODE} />
  261. </Form.Item>
  262. <Form.Item label={<strong>投放目标出价</strong>} required>
  263. <Card bordered className="cardResetCss newCss" bodyStyle={{ padding: 0, backgroundColor: '#fafafa' }}>
  264. {smartDeliveryGoalSpecRules.map((item, index) => {
  265. return <div className={style.newSpace} key={index}>
  266. <div className={style.newSpace_top}>
  267. <div className={style.newSpace_title}>{item.title}</div>
  268. {bidAllocationMode === 1 ? <Form.Item
  269. style={{ marginBottom: 0 }}
  270. name={['smartDeliverySceneSpec', 'smartDeliveryGoalSpec', smartDeliveryGoalSpecName, toCamelCase(item.field_name)]}
  271. rules={[
  272. { required: item.required, message: `请输入${item.title}` },
  273. {
  274. validator: (_: any, value: string) => {
  275. if (value === undefined || value === null || value === '') return Promise.resolve();
  276. const num = parseFloat(value);
  277. if (isNaN(num)) return Promise.reject(new Error('请输入有效的数字'));
  278. if (num < item.min || num > item.max) return Promise.reject(new Error(`范围${item.min}~${item.max}`));
  279. return Promise.resolve();
  280. }
  281. },
  282. {
  283. validator: (_: any, value: string) => {
  284. const regex = new RegExp(`^\\d+(\\.\\d{0,${item.decimal_length}})?$`);
  285. if (!value || regex.test(value)) {
  286. return Promise.resolve();
  287. }
  288. return Promise.reject(new Error(`请输入最多${item.decimal_length}位小数`));
  289. }
  290. }
  291. ]}
  292. >
  293. <Input style={{ width: 480 }} placeholder={item.placeholder} suffix={item.unitTips} />
  294. </Form.Item> : <Space>
  295. <Form.Item
  296. style={{ marginBottom: 0 }}
  297. name={['smartDeliverySceneSpec', 'smartDeliveryGoalSpec', smartDeliveryGoalSpecName, toCamelCase(item.field_name + '_min')]}
  298. rules={[
  299. { required: item.required, message: `请输入${item.title}最小值` },
  300. {
  301. validator: (_: any, value: string) => {
  302. if (value === undefined || value === null || value === '') return Promise.resolve();
  303. const num = parseFloat(value);
  304. if (isNaN(num)) return Promise.reject(new Error('最小值请输入有效的数字'));
  305. if (num < item.min || num > item.max) return Promise.reject(new Error(`最小值范围${item.min}~${item.max}`));
  306. return Promise.resolve();
  307. }
  308. },
  309. {
  310. validator: (_: any, value: string) => {
  311. const regex = new RegExp(`^\\d+(\\.\\d{0,${item.decimal_length}})?$`);
  312. if (!value || regex.test(value)) {
  313. return Promise.resolve();
  314. }
  315. return Promise.reject(new Error(`最小值请输入最多${item.decimal_length}位小数`));
  316. }
  317. }
  318. ]}
  319. noStyle
  320. >
  321. <Input style={{ width: 240 }} placeholder={item.placeholder + '最小值'} />
  322. </Form.Item>
  323. <span>-</span>
  324. <Form.Item
  325. style={{ marginBottom: 0 }}
  326. name={['smartDeliverySceneSpec', 'smartDeliveryGoalSpec', smartDeliveryGoalSpecName, toCamelCase(item.field_name + '_max')]}
  327. rules={[
  328. { required: item.required, message: `请输入${item.title}最大值` },
  329. {
  330. validator: (_: any, value: string) => {
  331. if (value === undefined || value === null || value === '') return Promise.resolve();
  332. const num = parseFloat(value);
  333. if (isNaN(num)) return Promise.reject(new Error('最大值请输入有效的数字'));
  334. if (num < item.min || num > item.max) return Promise.reject(new Error(`最大值范围${item.min}~${item.max}`));
  335. return Promise.resolve();
  336. }
  337. },
  338. {
  339. validator: (_: any, value: string) => {
  340. const regex = new RegExp(`^\\d+(\\.\\d{0,${item.decimal_length}})?$`);
  341. if (!value || regex.test(value)) {
  342. return Promise.resolve();
  343. }
  344. return Promise.reject(new Error(`最大值请输入最多${item.decimal_length}位小数`));
  345. }
  346. }
  347. ]}
  348. noStyle
  349. >
  350. <Input style={{ width: 240 }} placeholder={item.placeholder + '最大值'} />
  351. </Form.Item>
  352. <span>{item.unitTips}</span>
  353. </Space>}
  354. </div>
  355. </div>
  356. })}
  357. </Card>
  358. </Form.Item>
  359. </>}
  360. <Form.Item label={<strong>广告日预算</strong>} name='dailyBudget' rules={[{ required: (smartBidType === 'SMART_BID_TYPE_SYSTEMATIC' || bidScene === 'BID_SCENE_NORMAL_MAX'), message: '请输入广告日预算' }]}>
  361. <Input placeholder={`广告日预算${(smartBidType === 'SMART_BID_TYPE_SYSTEMATIC' || bidScene === 'BID_SCENE_NORMAL_MAX') ? '' : ', 不填默认为不限'}`} style={{ width: 480 }} suffix="元/天" />
  362. </Form.Item>
  363. </Card>
  364. }
  365. export default React.memo(AdgroupsPrice)