adActivityDialog.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. <script lang="ts" setup>
  2. /**
  3. * @Name: adActivityDialog.vue
  4. * @Description:广告关联活动弹窗
  5. * @Author: xinyan
  6. */
  7. import { computed, onMounted, reactive, ref, toRefs, watch } 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. // 处理表格复选框选择变化(是否为树形结构)
  153. const handleGridChange = (event) => {
  154. if (activeModel.value == 'specified' || activeModel.value == 'adGroup') {
  155. handleSelectionChange(event);
  156. } else {
  157. handelSelect(event);
  158. }
  159. };
  160. // 非树形结构的表格选择变化
  161. function handelSelect({ records }) {
  162. selectedAds.value = [
  163. ...selectedAds.value.filter(ad => ad.page !== currentPage.value),
  164. ...records.map(ad => ({ ...ad, page: currentPage.value }))
  165. ];
  166. }
  167. function updateSelectedAds() {
  168. selectedAds.value = gridOptions.data
  169. .filter(ad => ad.campaignGroupInfo && ad.campaignGroupInfo.some(group => group.isSelected))
  170. .map(ad => ({
  171. ...ad,
  172. campaignGroupInfo: ad.campaignGroupInfo.filter(group => group.isSelected),
  173. page: currentPage.value
  174. }));
  175. }
  176. // 树形结构的表格选择变化
  177. function handleSelectionChange({ records }) {
  178. selectedGroups = selectedGroups.filter(group => {
  179. return records.some(record => record.adGroupId === group.adGroupId);
  180. });
  181. let updatedRecords = [];
  182. const parentCampaignMap = new Map();
  183. records.forEach(record => {
  184. if (record.adGroupId) {
  185. // 这是一个广告组(子节点)
  186. const parentCampaign = gridOptions.data.find(campaign =>
  187. campaign.campaignGroupInfo.some(group => group.adGroupId === record.adGroupId)
  188. );
  189. if (parentCampaign) {
  190. // 如果父节点已经存在,则将子节点添加到父节点的 campaignGroupInfo 中
  191. if (parentCampaignMap.has(parentCampaign.campaignId)) {
  192. parentCampaignMap.get(parentCampaign.campaignId).campaignGroupInfo.push(record);
  193. } else {
  194. // 如果父节点不存在,则创建一个新的父节点对象
  195. parentCampaignMap.set(parentCampaign.campaignId, {
  196. campaignType: parentCampaign.campaignType,
  197. campaignId: parentCampaign.campaignId,
  198. campaignName: parentCampaign.campaignName,
  199. campaignGroupInfo: [record]
  200. });
  201. }
  202. }
  203. }
  204. });
  205. // 将所有父节点对象添加到 updatedRecords
  206. updatedRecords = Array.from(parentCampaignMap.values());
  207. // 更新选中的广告
  208. selectedAds.value = [
  209. ...updatedRecords.map(ad => ({ ...ad, page: currentPage.value })),
  210. ];
  211. }
  212. // 选择定向按钮
  213. function handleSelectTarget(row) {
  214. // 获取父节点数据
  215. const parent = gridOptions.data.find(campaign =>
  216. campaign.campaignGroupInfo.some(group => group.adGroupId === row.adGroupId)
  217. );
  218. selectedTargetedRow.value = {
  219. campaignType: parent.campaignType,
  220. campaignId: parent.campaignId,
  221. adGroupId: row.adGroupId,
  222. isSelected: row.isSelected || false, // 同步 isSelected 状态
  223. };
  224. targetRuleDialogVisible.value = true;
  225. }
  226. // 选择定向弹窗确认按钮
  227. function handleConfirm({ campaignInfo, targetType }) {
  228. if (!selectedTargetedRow.value) return;
  229. // 找到父级广告活动
  230. const parentCampaign = gridOptions.data.find(campaign =>
  231. campaign.campaignGroupInfo.some(group => group.adGroupId === selectedTargetedRow.value.adGroupId)
  232. );
  233. if (parentCampaign) {
  234. // 更新子节点(广告组)的信息
  235. const group = parentCampaign.campaignGroupInfo.find(group => group.adGroupId === selectedTargetedRow.value.adGroupId);
  236. if (group) {
  237. if (targetType === 'keyword') {
  238. group.keywordInfo = campaignInfo;
  239. } else if (targetType === 'target') {
  240. group.campaignTargetInfo = campaignInfo;
  241. }
  242. group.isSelected = true; // 更新子节点的选择状态
  243. group.targetLength = (group.keywordInfo?.length || 0) + (group.campaignTargetInfo?.length || 0);
  244. }
  245. // 勾选表格1中的对应行,只有在定向大于0时进行勾选
  246. if (group && group.targetLength > 0) {
  247. if (xGridOne.value) {
  248. xGridOne.value.toggleCheckboxRow(group, true); // 手动勾选复选框
  249. }
  250. }
  251. }
  252. // 更新选中的广告
  253. updateSelectedAds();
  254. }
  255. // 删除选中的广告
  256. const removeSelectedAd = async (row) => {
  257. const $grid = xGridTwo.value;
  258. if ($grid) {
  259. if (row.adGroupId) {
  260. // 删除子节点(广告组)
  261. selectedAds.value = selectedAds.value.map(ad => {
  262. if (ad.campaignGroupInfo) {
  263. return {
  264. ...ad,
  265. campaignGroupInfo: ad.campaignGroupInfo.filter(group => group.adGroupId !== row.adGroupId)
  266. };
  267. }
  268. return ad;
  269. }).filter(ad => ad.campaignGroupInfo && ad.campaignGroupInfo.length > 0);
  270. } else {
  271. // 删除父节点(广告活动)
  272. selectedAds.value = selectedAds.value.filter(ad => ad.campaignId !== row.campaignId);
  273. }
  274. await $grid.remove(row);
  275. }
  276. if (xGridOne.value) {
  277. await xGridOne.value.toggleCheckboxRow(row);
  278. }
  279. };
  280. function removeAllSelectedAds() {
  281. selectedAds.value = [];
  282. const $grid = xGridOne.value;
  283. if ($grid) {
  284. $grid.clearCheckboxRow();
  285. }
  286. }
  287. function cancel() {
  288. dialogVisible.value = false;
  289. }
  290. //TODO: 确认按钮-商品
  291. async function confirm() {
  292. const campaignItems = selectedAds.value.map(ad => ({
  293. campaignId: ad.campaignId,
  294. campaignType: ad.campaignType
  295. }));
  296. const adGroupInfo = [];
  297. const campaignTargetInfo = [
  298. { targetId: '492707808377423', adGroup_id: '448117369011017', bid: 0.45 },
  299. ];
  300. const campaignInfo = selectedTargetedRow.value.keywordInfo.map(keyword => ({
  301. keywordId: keyword.keywordId,
  302. adGroup_id: keyword.adGroup_id,
  303. bid: keyword.bid
  304. }));
  305. const requestData = {
  306. profileId: profile.value.profile_id,
  307. templateId: templateId.value,
  308. campaignItems,
  309. adGroupInfo,
  310. campaignTargetInfo,
  311. campaignInfo
  312. };
  313. console.log('requestData', requestData);
  314. }
  315. // 获取广告组下拉框
  316. async function fetchAdGroupList() {
  317. try {
  318. const resp = await getAdGroupList({
  319. profileId: profile.value.profile_id,
  320. });
  321. adGroups.value = resp.data.map((item: any) => {
  322. return {
  323. label: item.name,
  324. value: item.portfolioId
  325. };
  326. });
  327. } catch (error) {
  328. ElMessage.error('请求失败');
  329. }
  330. }
  331. const headerCellStyle = (args) => {
  332. if (args.rowIndex === 0) {
  333. return {
  334. backgroundColor: 'rgba(245, 245, 245, 0.9)',
  335. fontWeight: '500',
  336. };
  337. }
  338. };
  339. const cellStyle = () => {
  340. return {
  341. fontSize: '13px',
  342. //fontWeight: '500',
  343. };
  344. };
  345. //监控筛选条件变化
  346. watch(selectedCampaignType, () => {
  347. fetchAdCampaign();
  348. });
  349. watch(selectedAdGroup, (newVal) => {
  350. if (newVal) {
  351. selectedAdGroup.value = newVal;
  352. fetchAdCampaign();
  353. }
  354. });
  355. watch(selectedStatus, () => {
  356. fetchAdCampaign();
  357. });
  358. watch(templateId, () => {
  359. fetchAdCampaign();
  360. fetchAdGroupList();
  361. });
  362. watch(() => props.modelValue, (newValue) => {
  363. dialogVisible.value = newValue;
  364. });
  365. watch(dialogVisible, (newValue) => {
  366. emits('update:modelValue', newValue);
  367. });
  368. onMounted(() => {
  369. fetchAdGroupList();
  370. fetchAdCampaign();
  371. });
  372. </script>
  373. <template>
  374. <el-dialog
  375. v-model="dialogVisible"
  376. style="border-radius: 10px;"
  377. title="关联广告活动"
  378. width="1158px"
  379. >
  380. <div class="container">
  381. <div class="container-left">
  382. <el-input v-model="searchAdCampaign" clearable placeholder="请输入广告活动" style="width: 100%;"
  383. @change="handleAdCampaignChange()"></el-input>
  384. <div class="custom-inline">
  385. <el-select v-model="selectedCampaignType" placeholder="选择广告类型">
  386. <el-option v-for="item in campaignType"
  387. :key="item.value"
  388. :label="item.label"
  389. :value="item.value"
  390. ></el-option>
  391. </el-select>
  392. <el-select v-model="selectedAdGroup" clearable placeholder="广告组合" style="margin-bottom: 10px;">
  393. <el-option
  394. v-for="item in adGroups"
  395. :key="item.value"
  396. :label="item.label"
  397. :value="item.value"
  398. ></el-option>
  399. </el-select>
  400. <el-select v-model="selectedStatus" placeholder="状态" style="margin-bottom: 10px;">
  401. <el-option
  402. v-for="item in campaignStatus"
  403. :key="item.value"
  404. :label="item.label"
  405. :value="item.value"
  406. ></el-option>
  407. </el-select>
  408. </div>
  409. <vxe-grid ref="xGridOne" :cell-style="cellStyle" :header-cell-style="headerCellStyle" v-bind="gridOptions"
  410. @checkbox-change="handleGridChange" @checkbox-all="handleGridChange">
  411. <template #campaignName_default="{ row }">
  412. <el-tag
  413. v-if="row.campaignType"
  414. :color="row.campaignType === 'sb' ? '#0163d2' : (row.campaignType === 'sp' ? '#ff7424' : '#365672')"
  415. class="campaign-type"
  416. disable-transitions
  417. round>
  418. {{ row.campaignType }}
  419. </el-tag>
  420. <span> {{ row.campaignName }}</span>
  421. <span class="flex-container">
  422. {{ row.adGroupName }}
  423. <el-button
  424. v-if="row.adGroupName && activeModel==='specified'&&!row.isSelected"
  425. class="btn-link"
  426. link
  427. @click="handleSelectTarget(row)">
  428. 选择定向
  429. </el-button>
  430. <span v-else-if="row.adGroupName">已选择定向</span>
  431. </span>
  432. </template>
  433. </vxe-grid>
  434. <div class="pagination-container mt-4">
  435. <el-pagination
  436. v-model:current-page="currentPage"
  437. :page-size="pageSize"
  438. :page-sizes="[10, 25, 50,100,200]"
  439. :total="total"
  440. background
  441. layout="total,sizes,prev, next, jumper"
  442. small
  443. @size-change="handleSizeChange"
  444. @current-change="handleCurrentChange"
  445. />
  446. </div>
  447. </div>
  448. <div class="container-right">
  449. <h3>已选择({{ selectedAds.length }})</h3>
  450. <vxe-grid ref="xGridTwo"
  451. :cell-style="cellStyle"
  452. :columns="selectedColumns"
  453. :data="selectedAds"
  454. :header-cell-style="headerCellStyle"
  455. :tree-config="treeProps"
  456. border="inner"
  457. height="484">
  458. <template #campaignName_default="{ row }">
  459. <template v-if="!row.adGroupId">
  460. <!-- 父节点(广告活动) -->
  461. <el-tag
  462. v-if="row.campaignType"
  463. :color="row.campaignType === 'sb' ? '#0163d2' : (row.campaignType === 'sp' ? '#ff7424' : '#365672')"
  464. class="campaign-type"
  465. disable-transitions
  466. round>
  467. {{ row.campaignType }}
  468. </el-tag>
  469. <span>{{ row.campaignName }}</span>
  470. </template>
  471. <template v-else>
  472. <!-- 子节点(广告组) -->
  473. <div class="flex-container">
  474. <span>{{ row.adGroupName }}</span>
  475. <el-button
  476. v-if="row.isSelected"
  477. :icon="DocumentAdd"
  478. class="btn-link"
  479. link
  480. @click="handleSelectTarget(row)">
  481. 共{{ row.targetLength }}个定向规则
  482. </el-button>
  483. </div>
  484. </template>
  485. </template>
  486. <template #header_operation>
  487. <el-button link size="default" style="color: #2077d7" @click="removeAllSelectedAds">删除全部</el-button>
  488. </template>
  489. <template #default_operation="{row}">
  490. <el-button type="text" @click="removeSelectedAd(row)">
  491. <CircleClose style="width:16px;color:#4b5765" />
  492. </el-button>
  493. </template>
  494. </vxe-grid>
  495. </div>
  496. </div>
  497. <template #footer>
  498. <div class="dialog-footer">
  499. <el-button @click="cancel">取消</el-button>
  500. <el-button type="primary" @click="confirm">确定</el-button>
  501. </div>
  502. </template>
  503. </el-dialog>
  504. <!--选择定向弹窗-->
  505. <TargetRuleDialog v-if="activeModel === 'specified'"
  506. v-model="targetRuleDialogVisible"
  507. :selectedTargetedRow="selectedTargetedRow"
  508. @confirm:targetRule="handleConfirm"
  509. ></TargetRuleDialog>
  510. </template>
  511. <style scoped>
  512. .pagination-container {
  513. display: flex;
  514. flex-direction: row-reverse;
  515. }
  516. .custom-inline {
  517. display: flex;
  518. justify-content: space-around;
  519. margin: 12px 0;
  520. gap: 4px;
  521. }
  522. .campaign-type {
  523. /* width: 35px; */
  524. /* text-align: center; */
  525. /* height: 22px; */
  526. /* font-size: 13px; */
  527. /* font-weight: 400; */
  528. color: #fff;
  529. border-color: #fff;
  530. /* line-height: 21px; */
  531. border-radius: 12px;
  532. margin-right: 4px;
  533. }
  534. .container {
  535. width: 100%;
  536. height: 100%;
  537. border: 1px solid #d6dbe2;
  538. border-radius: 4px;
  539. display: flex;
  540. overflow: hidden;
  541. align-content: center;
  542. flex-wrap: nowrap;
  543. flex-direction: row;
  544. /* padding: 10px; */
  545. }
  546. .container-left {
  547. width: 50%;
  548. border-right: 1px solid #d6dbe2;
  549. padding: 15px
  550. }
  551. .container-right {
  552. flex: 1;
  553. padding: 15px
  554. }
  555. .btn-link {
  556. font-size: 13px;
  557. color: #0085ff;
  558. /* ling-heigt: 23px; */
  559. }
  560. .flex-container {
  561. display: flex;
  562. justify-content: space-between;
  563. }
  564. </style>