adActivityDialog.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <script lang="ts" setup>
  2. /**
  3. * @Name: adActivityDialog.vue
  4. * @Description:广告关联活动弹窗
  5. * @Author: xinyan
  6. */
  7. import { computed, onMounted, ref, watch ,toRefs} from 'vue';
  8. import { getAdGroupList, getRelationCampaign } from '/@/views/efTools/automation/api';
  9. import { storeToRefs } from 'pinia';
  10. import { useShopInfo } from '/@/stores/shopInfo';
  11. import { ElMessage } from 'element-plus';
  12. import TargetRuleDialog from '/@/views/efTools/automation/components/targetRuleDialog.vue';
  13. import { DocumentAdd } from '@element-plus/icons-vue';
  14. const props = defineProps({
  15. templateId: {
  16. type: [String, Number],
  17. required: true,
  18. },
  19. activeModel: {
  20. type: String,
  21. },
  22. modelValue: {
  23. type: Boolean,
  24. required: true,
  25. },
  26. });
  27. const emits = defineEmits(['update:modelValue']);
  28. const shopInfo = useShopInfo();
  29. const { profile } = storeToRefs(shopInfo);
  30. const { templateId } = toRefs(props);
  31. // const { activeModel } = toRefs(props);
  32. const activeModel = ref('specified');
  33. const dialogVisible = ref(true);
  34. const targetRuleDialogVisible = ref(false);
  35. // 定向规则
  36. //const adGroupId = ref('')
  37. //const campaignId = ref('')
  38. const selectedTargetedRow = ref(null);
  39. const targetLength = ref(0);
  40. let selectedGroups = [];
  41. // 筛选条件
  42. const searchAdCampaign = ref('');
  43. const selectedCampaignType = ref('sp');
  44. const selectedAdGroup = ref('');
  45. const selectedStatus = ref('');
  46. const adCampaignName = ref([]);
  47. const campaignType = [
  48. { value: 'sb', label: 'SB' },
  49. { value: 'sp', label: 'SP' },
  50. { value: 'sd', label: 'SD' },
  51. ];
  52. const adGroups = ref([]);
  53. const campaignStatus = [
  54. { value: 0, label: '未存档' },
  55. { value: 'ENABLED', label: '已启用' },
  56. { value: 'PAUSED', label: '已暂停' },
  57. ];
  58. // 表格
  59. const currentPage = ref(1);
  60. const pageSize = ref(25);
  61. const total = ref(0);
  62. const loading = ref(false);
  63. const xGridOne = ref(null);
  64. const xGridTwo = ref(null);
  65. let selectedAds = ref([]);
  66. const selectedColumns = computed(() => [
  67. {
  68. field: 'campaignName',
  69. title: '广告活动',
  70. slots: { default: 'campaignName_default' },
  71. treeNode: activeModel.value == 'specified' || activeModel.value == 'adGroup'
  72. },
  73. { title: '操作', width: 100, align: 'center', slots: { header: 'header_operation', default: 'default_operation' } }
  74. ]);
  75. const treeProps = computed(() => {
  76. activeModel.value = 'specified';
  77. if (activeModel.value === 'adGroup' || activeModel.value === 'specified') {
  78. return {
  79. rowField: 'campaignId',
  80. childrenField: 'campaignGroupInfo', // 子节点字段
  81. expandAll: true,
  82. };
  83. }
  84. return {};
  85. });
  86. const gridOptions = reactive({
  87. border: 'inner',
  88. height: 500,
  89. rowConfig: {
  90. isHover: true,
  91. keyField: 'campaignId',
  92. },
  93. treeConfig: treeProps,
  94. checkboxConfig: {
  95. labelField: 'name',
  96. reserve: true,
  97. checkStrictly: false,
  98. },
  99. columns: computed(() => [
  100. {
  101. field: 'campaignName',
  102. title: '广告活动',
  103. slots: { default: 'campaignName_default' },
  104. treeNode: activeModel.value == 'specified' || activeModel.value == 'adGroup'
  105. },
  106. { type: 'checkbox', width: 55, fixed: 'right' },
  107. ]),
  108. data: []
  109. });
  110. function handleCurrentChange(newPage) {
  111. currentPage.value = newPage;
  112. loading.value = true;
  113. fetchAdCampaign();
  114. }
  115. // 处理分页器每页显示条目数变化
  116. function handleSizeChange(newSize) {
  117. pageSize.value = newSize;
  118. currentPage.value = 1;
  119. fetchAdCampaign();
  120. }
  121. function handleAdCampaignChange() {
  122. localStorage.setItem('searchAdCampaign', JSON.stringify(searchAdCampaign.value));
  123. fetchAdCampaign();
  124. }
  125. async function fetchAdCampaign() {
  126. const savedAdCampaign = localStorage.getItem('searchAdCampaign');
  127. if (savedAdCampaign) {
  128. searchAdCampaign.value = JSON.parse(savedAdCampaign);
  129. }
  130. try {
  131. loading.value = true;
  132. const cachedSelectedAds = [...selectedAds.value];
  133. const resp = await getRelationCampaign({
  134. profileId: profile.value.profile_id,
  135. templateId: 51,
  136. campaignName: searchAdCampaign.value,
  137. portfolioId: selectedAdGroup.value,
  138. campaignStatus: selectedStatus.value,
  139. campaignType: selectedCampaignType.value,
  140. page: currentPage.value,
  141. limit: pageSize.value,
  142. });
  143. gridOptions.data = resp.data;
  144. total.value = resp.total;
  145. currentPage.value = resp.page;
  146. } catch (error) {
  147. ElMessage.error('请求广告活动数据失败');
  148. } finally {
  149. loading.value = false;
  150. }
  151. }
  152. const handleGridChange = (event) => {
  153. if (activeModel.value == 'specified' || activeModel.value == 'adGroup') {
  154. handleSelectionChange(event);
  155. } else {
  156. handelSelect(event);
  157. }
  158. };
  159. function handelSelect({ records }) {
  160. selectedAds.value = [
  161. ...selectedAds.value.filter(ad => ad.page !== currentPage.value),
  162. ...records.map(ad => ({ ...ad, page: currentPage.value }))
  163. ];
  164. }
  165. function updateSelectedAds() {
  166. selectedAds.value = gridOptions.data
  167. .filter(ad => ad.campaignGroupInfo && ad.campaignGroupInfo.some(group => group.isSelected))
  168. .map(ad => ({
  169. ...ad,
  170. campaignGroupInfo: ad.campaignGroupInfo.filter(group => group.isSelected),
  171. page: currentPage.value
  172. }));
  173. }
  174. function handleSelectionChange({ records, checked, reserve }) {
  175. console.log("=>(adActivityDialog.vue:193) records", records);
  176. if (records.length === 0 && !reserve) {
  177. // 当所有项目被取消选择时,清空 selectedAds
  178. selectedAds.value = [];
  179. } else {
  180. records.forEach(record => {
  181. if (record.adGroupId) {
  182. // 这是一个广告组(子节点)
  183. const parentCampaign = gridOptions.data.find(campaign =>
  184. campaign.campaignGroupInfo.some(group => group.adGroupId === record.adGroupId)
  185. );
  186. if (parentCampaign) {
  187. const group = parentCampaign.campaignGroupInfo.find(group => group.adGroupId === record.adGroupId);
  188. if (group) {
  189. group.isSelected = checked;
  190. }
  191. }
  192. } else {
  193. // 这是一个广告活动(父节点)
  194. record.campaignGroupInfo.forEach(group => {
  195. group.isSelected = checked;
  196. });
  197. }
  198. });
  199. updateSelectedAds();
  200. }
  201. console.log("=>(adActivityDialog.vue:224) selectedAds.value", selectedAds.value);
  202. }
  203. function handleSelectTarget(row) {
  204. // 获取父节点数据
  205. const parent = gridOptions.data.find(campaign =>
  206. campaign.campaignGroupInfo.some(group => group.adGroupId === row.adGroupId)
  207. );
  208. selectedTargetedRow.value = {
  209. campaignType: parent.campaignType,
  210. campaignId: parent.campaignId,
  211. adGroupId: row.adGroupId,
  212. isSelected: row.isSelected || false, // 同步 isSelected 状态
  213. };
  214. targetRuleDialogVisible.value = true;
  215. }
  216. function handleConfirm({ campaignInfo, targetType }) {
  217. if (!selectedTargetedRow.value) return;
  218. // 找到父级广告活动
  219. const parentCampaign = gridOptions.data.find(campaign =>
  220. campaign.campaignGroupInfo.some(group => group.adGroupId === selectedTargetedRow.value.adGroupId)
  221. );
  222. if (parentCampaign) {
  223. // 更新子节点(广告组)的信息
  224. const group = parentCampaign.campaignGroupInfo.find(group => group.adGroupId === selectedTargetedRow.value.adGroupId);
  225. if (group) {
  226. if (targetType === 'keyword') {
  227. group.keywordInfo = campaignInfo;
  228. } else if (targetType === 'target') {
  229. group.campaignTargetInfo = campaignInfo;
  230. }
  231. group.isSelected = true;
  232. group.targetLength = (group.keywordInfo?.length || 0) + (group.campaignTargetInfo?.length || 0);
  233. }
  234. }
  235. // 更新选中的广告
  236. updateSelectedAds();
  237. // 勾选表格1中的对应行
  238. if (xGridOne.value) {
  239. xGridOne.value.setCheckboxRow(selectedTargetedRow.value, true);
  240. }
  241. }
  242. const removeSelectedAd = async (row) => {
  243. console.log("=>(adActivityDialog.vue:274) row", row);
  244. const $grid = xGridTwo.value;
  245. if ($grid) {
  246. console.log("=>(1) selectedAds.value", selectedAds.value);
  247. if (row.adGroupId) {
  248. // 删除子节点(广告组)
  249. selectedAds.value = selectedAds.value.map(ad => {
  250. if (ad.campaignGroupInfo) {
  251. return {
  252. ...ad,
  253. campaignGroupInfo: ad.campaignGroupInfo.filter(group => group.adGroupId !== row.adGroupId)
  254. };
  255. }
  256. return ad;
  257. }).filter(ad => ad.campaignGroupInfo && ad.campaignGroupInfo.length > 0);
  258. } else {
  259. // 删除父节点(广告活动)
  260. selectedAds.value = selectedAds.value.filter(ad => ad.campaignId !== row.campaignId);
  261. }
  262. await $grid.remove(row);
  263. console.log("=>(2) selectedAds.value", selectedAds.value);
  264. }
  265. if (xGridOne.value) {
  266. await xGridOne.value.toggleCheckboxRow(row);
  267. }
  268. };
  269. function removeAllSelectedAds() {
  270. selectedAds.value = [];
  271. const $grid = xGridOne.value;
  272. if ($grid) {
  273. $grid.clearCheckboxRow();
  274. }
  275. }
  276. function cancel() {
  277. dialogVisible.value = false;
  278. }
  279. //TODO: 确认按钮-商品
  280. async function confirm() {
  281. const campaignItems = selectedAds.value.map(ad => ({
  282. campaignId: ad.campaignId,
  283. campaignType: ad.campaignType
  284. }));
  285. const adGroupInfo = [];
  286. const campaignTargetInfo = [
  287. { targetId: '492707808377423', adGroup_id: '448117369011017', bid: 0.45 },
  288. ];
  289. console.log('=>(adActivityDialog.vue:291) selectedTargetedRow.value', selectedTargetedRow.value);
  290. const campaignInfo = selectedTargetedRow.value.keywordInfo.map(keyword => ({
  291. keywordId: keyword.keywordId,
  292. adGroup_id: keyword.adGroup_id,
  293. bid: keyword.bid
  294. }));
  295. const requestData = {
  296. profileId: profile.value.profile_id,
  297. templateId: templateId.value,
  298. campaignItems,
  299. adGroupInfo,
  300. campaignTargetInfo,
  301. campaignInfo
  302. };
  303. console.log('requestData', requestData);
  304. }
  305. // 获取广告组下拉框
  306. async function fetchAdGroupList() {
  307. try {
  308. const resp = await getAdGroupList({
  309. profileId: profile.value.profile_id,
  310. });
  311. adGroups.value = resp.data.map((item: any) => {
  312. return {
  313. label: item.name,
  314. value: item.portfolioId
  315. };
  316. });
  317. } catch (error) {
  318. ElMessage.error('请求失败');
  319. }
  320. }
  321. const headerCellStyle = (args) => {
  322. if (args.rowIndex === 0) {
  323. return {
  324. backgroundColor: 'rgba(245, 245, 245, 0.9)',
  325. fontWeight: '500',
  326. };
  327. }
  328. };
  329. const cellStyle = () => {
  330. return {
  331. fontSize: '13px',
  332. //fontWeight: '500',
  333. };
  334. };
  335. //监控筛选条件变化
  336. watch(selectedCampaignType, () => {
  337. fetchAdCampaign();
  338. });
  339. watch(selectedAdGroup, (newVal) => {
  340. if (newVal) {
  341. selectedAdGroup.value = newVal;
  342. fetchAdCampaign();
  343. }
  344. });
  345. watch(selectedStatus, () => {
  346. fetchAdCampaign();
  347. });
  348. watch(templateId, () => {
  349. fetchAdCampaign();
  350. fetchAdGroupList();
  351. });
  352. watch(() => props.modelValue, (newValue) => {
  353. dialogVisible.value = newValue;
  354. });
  355. watch(dialogVisible, (newValue) => {
  356. emits('update:modelValue', newValue);
  357. });
  358. onMounted(() => {
  359. fetchAdGroupList();
  360. fetchAdCampaign();
  361. });
  362. </script>
  363. <template>
  364. <el-dialog
  365. v-model="dialogVisible"
  366. style="border-radius: 10px;"
  367. title="关联广告活动"
  368. width="1158px"
  369. >
  370. <div class="container">
  371. <div class="container-left">
  372. <el-input v-model="searchAdCampaign" clearable placeholder="请输入广告活动" style="width: 100%;"
  373. @change="handleAdCampaignChange()"></el-input>
  374. <div class="custom-inline">
  375. <el-select v-model="selectedCampaignType" placeholder="选择广告类型">
  376. <el-option v-for="item in campaignType"
  377. :key="item.value"
  378. :label="item.label"
  379. :value="item.value"
  380. ></el-option>
  381. </el-select>
  382. <el-select v-model="selectedAdGroup" clearable placeholder="广告组合" style="margin-bottom: 10px;">
  383. <el-option
  384. v-for="item in adGroups"
  385. :key="item.value"
  386. :label="item.label"
  387. :value="item.value"
  388. ></el-option>
  389. </el-select>
  390. <el-select v-model="selectedStatus" placeholder="状态" style="margin-bottom: 10px;">
  391. <el-option
  392. v-for="item in campaignStatus"
  393. :key="item.value"
  394. :label="item.label"
  395. :value="item.value"
  396. ></el-option>
  397. </el-select>
  398. </div>
  399. <vxe-grid ref="xGridOne" :cell-style="cellStyle" :header-cell-style="headerCellStyle" v-bind="gridOptions"
  400. @checkbox-change="handleGridChange" @checkbox-all="handleGridChange">
  401. <template #campaignName_default="{ row }">
  402. <el-tag
  403. v-if="row.campaignType"
  404. :color="row.campaignType === 'sb' ? '#0163d2' : (row.campaignType === 'sp' ? '#ff7424' : '#365672')"
  405. class="campaign-type"
  406. disable-transitions
  407. round>
  408. {{ row.campaignType }}
  409. </el-tag>
  410. <span> {{ row.campaignName }}</span>
  411. <span class="flex-container">
  412. {{ row.adGroupName }}
  413. <el-button
  414. v-if="row.adGroupName && activeModel==='specified'&&!row.isSelected"
  415. class="btn-link"
  416. link
  417. @click="handleSelectTarget(row)">
  418. 选择定向
  419. </el-button>
  420. <span v-else-if="row.adGroupName">已选择定向</span>
  421. </span>
  422. </template>
  423. </vxe-grid>
  424. <div class="pagination-container mt-4">
  425. <el-pagination
  426. v-model:current-page="currentPage"
  427. :page-size="pageSize"
  428. :page-sizes="[10, 25, 50,100,200]"
  429. :total="total"
  430. background
  431. layout="total,sizes,prev, next, jumper"
  432. small
  433. @size-change="handleSizeChange"
  434. @current-change="handleCurrentChange"
  435. />
  436. </div>
  437. </div>
  438. <div class="container-right">
  439. <h3>已选择({{ selectedAds.length }})</h3>
  440. <vxe-grid ref="xGridTwo"
  441. :cell-style="cellStyle"
  442. :columns="selectedColumns"
  443. :data="selectedAds"
  444. :header-cell-style="headerCellStyle"
  445. :tree-config="treeProps"
  446. border="inner"
  447. height="484">
  448. <template #campaignName_default="{ row }">
  449. <template v-if="!row.adGroupId">
  450. <!-- 父节点(广告活动) -->
  451. <el-tag
  452. v-if="row.campaignType"
  453. :color="row.campaignType === 'sb' ? '#0163d2' : (row.campaignType === 'sp' ? '#ff7424' : '#365672')"
  454. class="campaign-type"
  455. disable-transitions
  456. round>
  457. {{ row.campaignType }}
  458. </el-tag>
  459. <span>{{ row.campaignName }}</span>
  460. </template>
  461. <template v-else>
  462. <!-- 子节点(广告组) -->
  463. <div class="flex-container">
  464. <span>{{ row.adGroupName }}</span>
  465. <el-button
  466. v-if="row.isSelected"
  467. :icon="DocumentAdd"
  468. class="btn-link"
  469. link
  470. @click="handleSelectTarget(row)">
  471. 共{{ row.targetLength }}个定向规则
  472. </el-button>
  473. </div>
  474. </template>
  475. </template>
  476. <template #header_operation>
  477. <el-button link size="default" style="color: #2077d7" @click="removeAllSelectedAds">删除全部</el-button>
  478. </template>
  479. <template #default_operation="{row}">
  480. <el-button type="text" @click="removeSelectedAd(row)">
  481. <CircleClose style="width:16px;color:#4b5765" />
  482. </el-button>
  483. </template>
  484. </vxe-grid>
  485. </div>
  486. </div>
  487. <template #footer>
  488. <div class="dialog-footer">
  489. <el-button @click="cancel">取消</el-button>
  490. <el-button type="primary" @click="confirm">确定</el-button>
  491. </div>
  492. </template>
  493. </el-dialog>
  494. <TargetRuleDialog v-if="activeModel === 'specified'"
  495. v-model="targetRuleDialogVisible"
  496. :selectedTargetedRow="selectedTargetedRow"
  497. @confirm:targetRule="handleConfirm"
  498. ></TargetRuleDialog>
  499. </template>
  500. <style scoped>
  501. .pagination-container {
  502. display: flex;
  503. flex-direction: row-reverse;
  504. }
  505. .custom-inline {
  506. display: flex;
  507. justify-content: space-around;
  508. margin: 12px 0;
  509. gap: 4px;
  510. }
  511. .campaign-type {
  512. /* width: 35px; */
  513. /* text-align: center; */
  514. /* height: 22px; */
  515. /* font-size: 13px; */
  516. /* font-weight: 400; */
  517. color: #fff;
  518. border-color: #fff;
  519. /* line-height: 21px; */
  520. border-radius: 12px;
  521. margin-right: 4px;
  522. }
  523. .container {
  524. width: 100%;
  525. height: 100%;
  526. border: 1px solid #d6dbe2;
  527. border-radius: 4px;
  528. display: flex;
  529. overflow: hidden;
  530. align-content: center;
  531. flex-wrap: nowrap;
  532. flex-direction: row;
  533. /* padding: 10px; */
  534. }
  535. .container-left {
  536. width: 50%;
  537. border-right: 1px solid #d6dbe2;
  538. padding: 15px
  539. }
  540. .container-right {
  541. flex: 1;
  542. padding: 15px
  543. }
  544. .btn-link {
  545. font-size: 13px;
  546. color: #0085ff;
  547. /* ling-heigt: 23px; */
  548. }
  549. .flex-container {
  550. display: flex;
  551. justify-content: space-between;
  552. }
  553. </style>