index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. <template>
  2. <div class="layout-padding">
  3. <el-tabs v-model="activeName" type="card" class="demo-tabs" @tab-click="handleClick">
  4. <el-tab-pane :label="t('marketingConfig.ipList')" name="IP分组" class="layout-padding-auto layout-padding-view">
  5. <Title class="ml-4" :title="t('marketingConfig.ipList')" />
  6. <div class="p-4 rounded">
  7. <el-button style="margin-bottom: 10px;" type="primary" @click="onClickAdd('ip')">{{ t('marketingConfig.addIpList') }}</el-button><br>
  8. 筛选分组:<el-input placeholder="请输入分组名称" clearable
  9. style="display: inline-block; width: 200px; margin-left: 5px;"
  10. v-model="queryIPName" />
  11. <div v-if="ipData.length > 0" class="overflow-y-auto mt-2" style="height: calc(100vh - 300px)">
  12. <JCollapse
  13. @update="(item) => onClickEdit(item, 'ip')"
  14. @delete="(item) => onOpenDelete(item, 'ip')"
  15. :data="showIPData"
  16. :activeNames="ipActiveId"
  17. :deleteText="t('marketingConfig.deleteListText')"
  18. :updateText="t('marketingConfig.updateText')"
  19. />
  20. </div>
  21. </div>
  22. </el-tab-pane>
  23. <el-tab-pane :label="t('marketingConfig.domainList')" name="域名分组" class="layout-padding-auto layout-padding-view">
  24. <Title class="ml-4" :title="t('marketingConfig.domainList')" />
  25. <div class="p-4 rounded">
  26. <el-button style="margin-bottom: 10px;" type="primary" @click="onClickAdd('domain')">{{ t('marketingConfig.addDomainList') }}</el-button><br>
  27. 筛选分组:<el-input placeholder="请输入分组名称" clearable
  28. style="display: inline-block; width: 200px; margin-left: 5px;"
  29. v-model="queryDomainName" />
  30. <div v-if="domainData.length > 0" class="overflow-y-auto mt-2" style="height: calc(100vh - 300px)">
  31. <JCollapse
  32. @update="(item) => onClickEdit(item, 'domain')"
  33. :data="showDomainData"
  34. @delete="(item) => onOpenDelete(item, 'domain')"
  35. :activeNames="domainActiveId"
  36. :deleteText="t('marketingConfig.deleteListText')"
  37. :updateText="t('marketingConfig.updateText')"
  38. />
  39. </div>
  40. <div v-else class="ml-2">暂无数据</div>
  41. </div>
  42. </el-tab-pane>
  43. <el-tab-pane :label="t('marketingConfig.disposition')" name="全局配置" class="layout-padding-auto layout-padding-view">
  44. <Title class="ml-4" :title="t('marketingConfig.disposition')" />
  45. <div class="p-4 rounded overflow-y-auto" style="max-height: calc(100vh - 350px)">
  46. <JCollapse
  47. :data="[{ title: 'IP集合', id: '1' }]"
  48. :activeNames="['1']"
  49. @update="(item) => (listEditOpen = true)"
  50. @delete="(item) => (closeIpTags = !closeIpTags)"
  51. :deleteText="closeIpTags ? t('marketingConfig.cancel') : t('marketingConfig.deleteIp')"
  52. :updateText="t('marketingConfig.addIp')"
  53. >
  54. <template #default>
  55. <div class="border-b p-2 items-center flex flex-wrap">
  56. <span class="mr-2">白名单</span>
  57. <span v-for="item in ipWhiteList" :key="item.id">
  58. <el-popover v-if="item?.sourceType === 1" width="280" @show="onLoadDetail(item)" trigger="hover" placement="top">
  59. <div v-if="item.list.length > 0" class="flex flex-wrap">
  60. <span v-for="ip in item.list" :key="ip" class="ml-2">
  61. {{ ip }}
  62. </span>
  63. </div>
  64. <div v-else>暂无数据</div>
  65. <template #reference>
  66. <el-tag
  67. effect="light"
  68. color="#f4f4f4"
  69. :closable="closeIpTags"
  70. @close="handleDelete(item, 'ip')"
  71. round
  72. class="ml-1 cursor-pointer"
  73. >
  74. {{ item.groupName }}
  75. </el-tag>
  76. </template>
  77. </el-popover>
  78. <el-tag
  79. v-else
  80. effect="light"
  81. color="#f4f4f4"
  82. :closable="closeIpTags"
  83. @close="handleDelete(item, 'ip')"
  84. round
  85. class="ml-1 cursor-pointer"
  86. >
  87. {{ ipSplicing(item.startIp, item.endIp) }}
  88. </el-tag>
  89. </span>
  90. </div>
  91. <div class="p-2 items-center flex flex-wrap">
  92. <span class="mr-2">黑名单</span>
  93. <span v-for="item in ipBlackList" :key="item.id">
  94. <el-popover v-if="item?.sourceType === 1" width="280" @show="onLoadDetail(item, ipList)" trigger="hover" placement="top">
  95. <div v-if="item.list.length > 0" class="flex flex-wrap">
  96. <span v-for="ip in item.list" :key="ip" class="ml-2">
  97. {{ ip }}
  98. </span>
  99. </div>
  100. <div v-else>暂无数据</div>
  101. <template #reference>
  102. <el-tag
  103. effect="light"
  104. color="#f4f4f4"
  105. :closable="closeIpTags"
  106. @close="handleDelete(item, 'ip')"
  107. round
  108. class="ml-1 cursor-pointer"
  109. >
  110. {{ item.groupName }}
  111. </el-tag>
  112. </template>
  113. </el-popover>
  114. <el-tag
  115. v-else
  116. effect="light"
  117. color="#f4f4f4"
  118. :closable="closeIpTags"
  119. @close="handleDelete(item, 'ip')"
  120. round
  121. class="ml-1 cursor-pointer"
  122. >
  123. {{ ipSplicing(item.startIp, item.endIp) }}
  124. </el-tag>
  125. </span>
  126. </div>
  127. </template>
  128. </JCollapse>
  129. <JCollapse
  130. class="mt-4"
  131. :data="[{ title: '域名集合', id: '1' }]"
  132. :activeNames="['1']"
  133. @update="() => (domainEditOpen = true)"
  134. @delete="() => (closeDomainTags = !closeDomainTags)"
  135. :deleteText="closeDomainTags ? t('marketingConfig.cancel') : t('marketingConfig.deleteDomain')"
  136. :updateText="t('marketingConfig.addDomain')"
  137. >
  138. <template #default>
  139. <div class="p-2 items-center flex flex-wrap">
  140. <span v-if="domainList.length > 0">
  141. <span v-for="item in domainList" :key="item.id">
  142. <el-popover v-if="item?.sourceType === 1" width="280" @show="onLoadDetail(item)" trigger="hover" placement="top">
  143. <div v-if="item.list.length > 0" class="flex flex-wrap">
  144. <span v-for="domain in item.list" :key="domain.id" class="ml-2">
  145. {{ domain.domain }}
  146. </span>
  147. </div>
  148. <div v-else>暂无数据</div>
  149. <template #reference>
  150. <el-tag
  151. effect="light"
  152. color="#f4f4f4"
  153. :closable="closeDomainTags"
  154. @close="handleDelete(item, 'domain')"
  155. round
  156. class="ml-1 cursor-pointer"
  157. >
  158. {{ item.groupName }}
  159. </el-tag>
  160. </template>
  161. </el-popover>
  162. <el-tag
  163. v-else
  164. effect="light"
  165. color="#f4f4f4"
  166. :closable="closeDomainTags"
  167. @close="handleDelete(item, 'domain')"
  168. round
  169. class="ml-1 cursor-pointer"
  170. >
  171. {{ item.domain }}
  172. </el-tag>
  173. </span>
  174. </span>
  175. <div class="text-gray-400 ml-2" v-else>--</div>
  176. </div>
  177. </template>
  178. </JCollapse>
  179. </div>
  180. <div class="w-[66%] ml-[-8px] mt-5">
  181. <el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px" class="flex flex-wrap">
  182. <el-form-item :label="t('marketingConfig.jumpMode')" prop="triggerMode" class="w-1/3">
  183. <JDictSelect
  184. v-model:value="formData.triggerMode"
  185. :placeholder="t('marketingConfig.jumpModeTip')"
  186. :dictType="'triggerMode'"
  187. :selectFirst="true"
  188. :styleClass="'w-full'"
  189. />
  190. </el-form-item>
  191. <el-form-item :label="t('marketingConfig.triggerType')" prop="triggerRule" class="w-1/3">
  192. <JDictSelect
  193. v-model:value="formData.triggerRule"
  194. :placeholder="t('marketingConfig.triggerTypeTip')"
  195. :dictType="'triggerRule'"
  196. :selectFirst="true"
  197. :styleClass="'w-full'"
  198. />
  199. </el-form-item>
  200. <el-form-item :label="t('marketingConfig.triggerFrequency')" prop="triggerNum" class="w-1/3">
  201. <el-input v-model="formData.triggerNum" type="text" :placeholder="t('marketingConfig.triggerFrequencyTip')" />
  202. </el-form-item>
  203. <div class="w-full mb-[18px]">
  204. <el-form-item
  205. v-if="formData.triggerMode == '2' || formData.triggerMode == '3'"
  206. :label="t('marketingConfig.prompt')"
  207. prop="promptMsg"
  208. class="w-1/3"
  209. >
  210. <el-input v-model="formData.promptMsg" type="text" :placeholder="t('marketingConfig.promptTip')"></el-input>
  211. </el-form-item>
  212. <el-form-item
  213. v-if="formData.triggerMode == '1' || formData.triggerMode == '3'"
  214. :label="t('marketingConfig.jumpLink')"
  215. prop="url"
  216. class="w-1/3"
  217. >
  218. <el-input v-model="formData.url" type="text" :placeholder="t('marketingConfig.jumpLinkTip')"></el-input>
  219. </el-form-item>
  220. </div>
  221. <div class="w-full">
  222. <el-button type="primary" @click="onSubmit(ruleFormRef)" class="w-[80px] ml-5">{{ t('common.saveBtn') }}</el-button>
  223. </div>
  224. </el-form>
  225. </div>
  226. </el-tab-pane>
  227. </el-tabs>
  228. <DomainEdit :select-data="domainData" v-model:open="domainEditOpen" @onsuccess="getConfig" />
  229. <ListEdit :select-data="ipData" v-model:open="listEditOpen" @onsuccess="getConfig" />
  230. <GroupingEdit v-model:open="groupingEditOpen" :type="openType" @onsuccess="getData" />
  231. <IpListEdit :type="openType" ref="menuDialogRef" @onsuccess="getData" />
  232. <el-dialog v-model="delOpen" title="提示" width="500" @close="delOpen = false">
  233. <span>确认删除{{ delObj?.title }}吗?</span>
  234. <template #footer>
  235. <div class="dialog-footer">
  236. <el-button @click="delOpen = false">取消</el-button>
  237. <el-button type="primary" :disabled="loading" @click="onDel(delObj)"> 确定 </el-button>
  238. </div>
  239. </template>
  240. </el-dialog>
  241. </div>
  242. </template>
  243. <script lang="ts" name="marketingConfig" setup>
  244. import {
  245. delGroup,
  246. pageListDomain,
  247. pageListIp,
  248. getConfigIpList,
  249. getConfigDomainList,
  250. delIpList,
  251. delDomainList,
  252. getGroupDetail,
  253. getConfigDetail,
  254. saveConfigDetail,
  255. } from '/@/api/marketing/config';
  256. import { useI18n } from 'vue-i18n';
  257. import { useMessage } from '/@/hooks/message';
  258. import { rule } from '/@/utils/validate';
  259. import { ipSplicing } from '/@/utils/ipUpdate';
  260. // 引入组件
  261. const JCollapse = defineAsyncComponent(() => import('/@/components/JCollapse/index.vue'));
  262. const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
  263. const DomainEdit = defineAsyncComponent(() => import('./components/domainEdit.vue'));
  264. const ListEdit = defineAsyncComponent(() => import('./components/listEdit.vue'));
  265. const GroupingEdit = defineAsyncComponent(() => import('./components/ipGroupingEdit.vue'));
  266. const IpListEdit = defineAsyncComponent(() => import('./components/ipListEdit.vue'));
  267. const JDictSelect = defineAsyncComponent(() => import('/@/components/JDictSelect/index.vue'));
  268. const { t } = useI18n();
  269. // 定义变量内容
  270. const activeName = ref('IP分组');
  271. const queryIPName = ref('');
  272. const queryDomainName = ref('');
  273. const showIPData = computed(() => {
  274. return ipData.value.filter((item) => item.groupName.includes(queryIPName.value));
  275. });
  276. const showDomainData = computed(() => {
  277. return domainData.value.filter((item) => item.groupName.includes(queryDomainName.value));
  278. });
  279. //关闭或打开tabs的关闭按钮
  280. const closeDomainTags = ref(false);
  281. const closeIpTags = ref(false);
  282. // 弹窗
  283. const domainEditOpen = ref(false);
  284. const listEditOpen = ref(false);
  285. const groupingEditOpen = ref(false);
  286. const delOpen = ref(false);
  287. const loading = ref(false);
  288. const menuDialogRef = ref();
  289. const domainActiveId = ref([]);
  290. const ipActiveId = ref([]);
  291. // const ipListEditOpen = ref(false);
  292. const domainData = ref([]);
  293. const ipData = ref([]);
  294. const delObj = ref({});
  295. const openType = ref('ip'); // 'ip' or 'domain'
  296. const ipWhiteList = ref([]); // ip白名单
  297. const ipBlackList = ref([]); // ip黑名单
  298. const domainList = ref([]);
  299. const ruleFormRef = ref();
  300. const formData = ref({
  301. promptMsg: '',
  302. triggerMode: '',
  303. triggerRule: '',
  304. url: '',
  305. triggerNum: '',
  306. });
  307. const onOpenDelete = (item: any, type: any) => {
  308. console.log(item);
  309. delObj.value = { ...item, type };
  310. console.log(delObj.value);
  311. delOpen.value = true;
  312. };
  313. const getData = () => {
  314. openType.value === 'ip' ? getIpData() : getDomainData();
  315. };
  316. const onLoadDetail = async (item: any) => {
  317. if (item.list.length !== 0) return;
  318. await getGroupDetail({ id: item.groupId }).then((val) => {
  319. item.list = val.data.ips.map((item) => {
  320. return ipSplicing(item.startIp, item.endIp);
  321. });
  322. });
  323. };
  324. // // 表单校验规则
  325. const dataRules = reactive({
  326. url: [
  327. { required: true, message: '跳转连接不能为空', trigger: 'blur' },
  328. { validator: rule.domain, trigger: 'blur' },
  329. ],
  330. promptMsg: [
  331. { required: true, message: '提示信息不能为空', trigger: 'blur' },
  332. ],
  333. triggerNum: [
  334. { required: true, message: '触发频率不能为空', trigger: 'blur' },
  335. {
  336. pattern: /^(10000|[1-9]\d{0,3}|0)$|^(100%|[1-9]?\d%|0%)$/,
  337. message: '请输入 0-10000 的正整数或 0%-100% 的百分比',
  338. trigger: 'blur',
  339. },
  340. ],
  341. });
  342. const onSubmit = async () => {
  343. try {
  344. await ruleFormRef.value.validateField('triggerNum');
  345. } catch (error) {
  346. // 验证失败,阻止后续逻辑执行
  347. return;
  348. }
  349. if (formData.value.triggerMode === '1' || formData.value.triggerMode === '3') {
  350. try {
  351. await ruleFormRef.value.validateField('url');
  352. } catch (error) {
  353. // 验证失败,阻止后续逻辑执行
  354. return;
  355. }
  356. }
  357. try {
  358. loading.value = true;
  359. // 处理 triggerNum 参数
  360. let triggerNum = formData.value.triggerNum;
  361. if (typeof triggerNum === 'string' && triggerNum.includes('%')) {
  362. // 如果包含百分比符号,去除百分比符号并除以100
  363. const num = parseFloat(triggerNum.replace('%', ''));
  364. if (!isNaN(num)) {
  365. triggerNum = num / 100;
  366. }
  367. }
  368. await saveConfigDetail({
  369. ...formData.value,
  370. triggerNum: triggerNum.toString(),
  371. });
  372. useMessage().success(t('common.editSuccessText'));
  373. } catch (err) {
  374. useMessage().error(err.msg);
  375. } finally {
  376. loading.value = false;
  377. getConfig();
  378. }
  379. };
  380. const handleClick = (data: any) => {
  381. if (data.props.label === 'IP分组') {
  382. getIpData();
  383. } else if (data.props.label === '域名分组') {
  384. getDomainData();
  385. } else {
  386. getConfig();
  387. }
  388. };
  389. const handleDelete = async (item: any, type: string) => {
  390. console.log(item, type);
  391. delObj.value = { ...item, delListType: type };
  392. delOpen.value = true;
  393. };
  394. const onDel = async (data: any) => {
  395. if (data?.delListType) {
  396. try {
  397. if (data?.delListType === 'ip') {
  398. await delIpList(data.id);
  399. useMessage().success(t('common.delSuccessText'));
  400. } else if (data?.delListType === 'domain') {
  401. await delDomainList(data.id);
  402. useMessage().success(t('common.delSuccessText'));
  403. }
  404. } catch (err: any) {
  405. useMessage().error(err.msg);
  406. } finally {
  407. loading.value = false;
  408. delOpen.value = false;
  409. getConfig();
  410. }
  411. return;
  412. }
  413. try {
  414. loading.value = true;
  415. await delGroup({
  416. groupType: data.type === 'ip' ? 1 : 2,
  417. ids: [data.id],
  418. });
  419. useMessage().success(t('common.delSuccessText'));
  420. } catch (err: any) {
  421. useMessage().error(err.msg);
  422. } finally {
  423. loading.value = false;
  424. delOpen.value = false;
  425. data.type === 'ip' ? getIpData() : getDomainData();
  426. }
  427. };
  428. const onClickAdd = (type: string) => {
  429. openType.value = type;
  430. groupingEditOpen.value = true;
  431. };
  432. const onClickEdit = (item: any, type: string) => {
  433. openType.value = type;
  434. // ipListEditOpen.value = true;
  435. onOpenEditMenu(type, item);
  436. };
  437. // 打开编辑菜单弹窗
  438. const onOpenEditMenu = (type: string, row: any) => {
  439. menuDialogRef.value.openDialog(type, row);
  440. };
  441. const getConfig = () => {
  442. configIp();
  443. configDomain();
  444. };
  445. const configDomain = async () => {
  446. await getConfigDomainList().then((val) => {
  447. domainList.value = val.data.map((item) => {
  448. return {
  449. ...item,
  450. list: [],
  451. };
  452. });
  453. });
  454. };
  455. const configIp = async () => {
  456. await getConfigIpList().then((val) => {
  457. ipWhiteList.value = val.data
  458. .filter((item) => item.ipType === 1)
  459. .map((item) => {
  460. return { ...item, list: [] };
  461. });
  462. ipBlackList.value = val.data
  463. .filter((item) => item.ipType === 2)
  464. .map((item) => {
  465. return { ...item, list: [] };
  466. });
  467. });
  468. };
  469. const getDomainData = async () => {
  470. await pageListDomain().then((val) => {
  471. domainActiveId.value = [];
  472. domainData.value = val.data.map((item: any) => {
  473. domainActiveId.value.push(item.id);
  474. return {
  475. ...item,
  476. title: item.groupName,
  477. id: item.id,
  478. list: item.domains.map((items: any) => {
  479. return {
  480. ...items,
  481. id: items.id,
  482. value: items.domain,
  483. };
  484. }),
  485. };
  486. });
  487. });
  488. };
  489. const getIpData = async () => {
  490. await pageListIp().then((val) => {
  491. ipActiveId.value = [];
  492. ipData.value = val.data.map((item: any) => {
  493. ipActiveId.value.push(item.id);
  494. return {
  495. ...item,
  496. title: item.groupName,
  497. id: item.id,
  498. list: item.ips.map((items: any) => {
  499. return {
  500. ...items,
  501. id: items.id,
  502. value: ipSplicing(items.startIp, items.endIp),
  503. };
  504. }),
  505. };
  506. });
  507. });
  508. await getConfigDetail().then((val) => {
  509. formData.value = {
  510. promptMsg: '',
  511. triggerMode: '',
  512. triggerRule: '',
  513. url: '',
  514. triggerNum: '',
  515. };
  516. formData.value = {
  517. ...val.data,
  518. triggerMode: val.data?.triggerMode.toString(),
  519. triggerRule: val.data?.triggerRule.toString(),
  520. triggerNum: parseFloat(val.data?.triggerNum) < 1 ? parseFloat(val.data?.triggerNum) * 100 + '%' : val.data?.triggerNum,
  521. };
  522. });
  523. };
  524. onMounted(() => {
  525. //获取IP列表
  526. getIpData();
  527. getConfig();
  528. });
  529. </script>
  530. <style lang="scss">
  531. .is-top {
  532. margin-bottom: 0 !important;
  533. }
  534. .el-collapse-item__content {
  535. padding-bottom: 0 !important;
  536. }
  537. .el-tabs--card > .el-tabs__header .el-tabs__item {
  538. background-color: #fff;
  539. }
  540. .el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
  541. background-color: #e8f2fe;
  542. border-bottom-color: #e8f2fe;
  543. }
  544. </style>