|
@@ -0,0 +1,448 @@
|
|
|
|
+<template>
|
|
|
|
+ <el-dialog title="修改规则" width="1000" v-model="props.open" :close-on-click-modal="false" :destroy-on-close="true" @close="onCancel" draggable>
|
|
|
|
+ <div class="p-4 rounded overflow-y-auto" style="max-height: calc(100vh - 350px)">
|
|
|
|
+ <div class="flex items-start">
|
|
|
|
+ <label class="w-[50px] leading-8">关键字</label>
|
|
|
|
+ <div class="flex gap-2 ml-2 flex-wrap w-[59vw]">
|
|
|
|
+ <el-tag v-for="tag in dynamicTags" :key="tag" size="large" closable :disable-transitions="false" @close="handleClose(tag)">
|
|
|
|
+ {{ tag }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ <el-input
|
|
|
|
+ v-if="inputVisible"
|
|
|
|
+ ref="InputRef"
|
|
|
|
+ v-model="inputValue"
|
|
|
|
+ class="!w-32"
|
|
|
|
+ @keyup.enter="handleInputConfirm"
|
|
|
|
+ @blur="handleInputConfirm"
|
|
|
|
+ />
|
|
|
|
+ <el-button v-else class="button-new-tag" @click="showInput"> 添加关键字</el-button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <JCollapse
|
|
|
|
+ :data="[{ title: 'IP集合', id: '1' }]"
|
|
|
|
+ :activeNames="['1']"
|
|
|
|
+ @update="(item) => (listEditOpen = true)"
|
|
|
|
+ @delete="(item) => (closeIpTags = !closeIpTags)"
|
|
|
|
+ :deleteText="closeIpTags ? t('marketingConfig.cancel') : t('marketingConfig.deleteIp')"
|
|
|
|
+ :updateText="t('marketingConfig.addIp')"
|
|
|
|
+ >
|
|
|
|
+ <template #default>
|
|
|
|
+ <div class="border-b p-2 items-center flex flex-wrap">
|
|
|
|
+ <span class="mr-2">白名单</span>
|
|
|
|
+ <span v-for="item in ipWhiteList" :key="item.id">
|
|
|
|
+ <el-popover v-if="item?.sourceType === 1" width="280" @show="onLoadDetail(item)" trigger="hover" placement="top">
|
|
|
|
+ <div v-if="item.list.length > 0" class="flex flex-wrap">
|
|
|
|
+ <span v-for="ip in item.list" :key="ip" class="ml-2">
|
|
|
|
+ {{ ip }}
|
|
|
|
+ </span>
|
|
|
|
+ </div>
|
|
|
|
+ <div v-else>暂无数据</div>
|
|
|
|
+ <template #reference>
|
|
|
|
+ <el-tag effect="light" color="#f4f4f4" :closable="closeIpTags" @close="handleDelete(item, 'ip')" round class="ml-1 cursor-pointer">
|
|
|
|
+ {{ item.groupName }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ </template>
|
|
|
|
+ </el-popover>
|
|
|
|
+ <el-tag
|
|
|
|
+ v-else
|
|
|
|
+ effect="light"
|
|
|
|
+ color="#f4f4f4"
|
|
|
|
+ :closable="closeIpTags"
|
|
|
|
+ @close="handleDelete(item, 'ip')"
|
|
|
|
+ round
|
|
|
|
+ class="ml-1 cursor-pointer"
|
|
|
|
+ >
|
|
|
|
+ {{ ipSplicing(item.startIp, item.endIp) }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ </span>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="p-2 items-center flex flex-wrap">
|
|
|
|
+ <span class="mr-2">黑名单</span>
|
|
|
|
+
|
|
|
|
+ <span v-for="item in ipBlackList" :key="item.id">
|
|
|
|
+ <el-popover v-if="item?.sourceType === 1" width="280" @show="onLoadDetail(item, ipList)" trigger="hover" placement="top">
|
|
|
|
+ <div v-if="item.list.length > 0" class="flex flex-wrap">
|
|
|
|
+ <span v-for="ip in item.list" :key="ip" class="ml-2">
|
|
|
|
+ {{ ip }}
|
|
|
|
+ </span>
|
|
|
|
+ </div>
|
|
|
|
+ <div v-else>暂无数据</div>
|
|
|
|
+ <template #reference>
|
|
|
|
+ <el-tag effect="light" color="#f4f4f4" :closable="closeIpTags" @close="handleDelete(item, 'ip')" round class="ml-1 cursor-pointer">
|
|
|
|
+ {{ item.groupName }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ </template>
|
|
|
|
+ </el-popover>
|
|
|
|
+ <el-tag
|
|
|
|
+ v-else
|
|
|
|
+ effect="light"
|
|
|
|
+ color="#f4f4f4"
|
|
|
|
+ :closable="closeIpTags"
|
|
|
|
+ @close="handleDelete(item, 'ip')"
|
|
|
|
+ round
|
|
|
|
+ class="ml-1 cursor-pointer"
|
|
|
|
+ >
|
|
|
|
+ {{ ipSplicing(item.startIp, item.endIp) }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ </span>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ </JCollapse>
|
|
|
|
+ <JCollapse
|
|
|
|
+ class="mt-4"
|
|
|
|
+ :data="[{ title: '域名集合', id: '1' }]"
|
|
|
|
+ :activeNames="['1']"
|
|
|
|
+ @update="() => (domainEditOpen = true)"
|
|
|
|
+ @delete="() => (closeDomainTags = !closeDomainTags)"
|
|
|
|
+ :deleteText="closeDomainTags ? t('marketingConfig.cancel') : t('marketingConfig.deleteDomain')"
|
|
|
|
+ :updateText="t('marketingConfig.addDomain')"
|
|
|
|
+ >
|
|
|
|
+ <template #default>
|
|
|
|
+ <div class="p-2 items-center flex flex-wrap">
|
|
|
|
+ <span v-if="domainList.length > 0">
|
|
|
|
+ <span v-for="item in domainList" :key="item.id">
|
|
|
|
+ <el-popover v-if="item?.sourceType === 1" width="280" @show="onLoadDetail(item)" trigger="hover" placement="top">
|
|
|
|
+ <div v-if="item.list.length > 0" class="flex flex-wrap">
|
|
|
|
+ <span v-for="domain in item.list" :key="domain.id" class="ml-2">
|
|
|
|
+ {{ domain.domain }}
|
|
|
|
+ </span>
|
|
|
|
+ </div>
|
|
|
|
+ <div v-else>暂无数据</div>
|
|
|
|
+ <template #reference>
|
|
|
|
+ <el-tag
|
|
|
|
+ effect="light"
|
|
|
|
+ color="#f4f4f4"
|
|
|
|
+ :closable="closeDomainTags"
|
|
|
|
+ @close="handleDelete(item, 'domain')"
|
|
|
|
+ round
|
|
|
|
+ class="ml-1 cursor-pointer"
|
|
|
|
+ >
|
|
|
|
+ {{ item.groupName }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ </template>
|
|
|
|
+ </el-popover>
|
|
|
|
+ <el-tag
|
|
|
|
+ v-else
|
|
|
|
+ effect="light"
|
|
|
|
+ color="#f4f4f4"
|
|
|
|
+ :closable="closeDomainTags"
|
|
|
|
+ @close="handleDelete(item, 'domain')"
|
|
|
|
+ round
|
|
|
|
+ class="ml-1 cursor-pointer"
|
|
|
|
+ >
|
|
|
|
+ {{ item.domain }}
|
|
|
|
+ </el-tag>
|
|
|
|
+ </span>
|
|
|
|
+ </span>
|
|
|
|
+ <div class="text-gray-400 ml-2" v-else>--</div>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ </JCollapse>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="w-full ml-[-8px] mt-5">
|
|
|
|
+ <el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px" class="flex flex-wrap">
|
|
|
|
+ <el-form-item :label="t('marketingConfig.jumpMode')" prop="triggerMode" class="w-1/3">
|
|
|
|
+ <JDictSelect
|
|
|
|
+ v-model:value="formData.triggerMode"
|
|
|
|
+ :placeholder="t('marketingConfig.jumpModeTip')"
|
|
|
|
+ :dictType="'triggerMode'"
|
|
|
|
+ :selectFirst="true"
|
|
|
|
+ :styleClass="'w-full'"
|
|
|
|
+ />
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item :label="t('marketingConfig.triggerType')" prop="triggerRule" class="w-1/3">
|
|
|
|
+ <JDictSelect
|
|
|
|
+ v-model:value="formData.triggerRule"
|
|
|
|
+ :placeholder="t('marketingConfig.triggerTypeTip')"
|
|
|
|
+ :dictType="'triggerRule'"
|
|
|
|
+ :selectFirst="true"
|
|
|
|
+ :styleClass="'w-full'"
|
|
|
|
+ />
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item :label="t('marketingConfig.triggerFrequency')" prop="triggerNum" class="w-1/3">
|
|
|
|
+ <el-input v-model="formData.triggerNum" type="text" :placeholder="t('marketingConfig.triggerFrequencyTip')" />
|
|
|
|
+ </el-form-item>
|
|
|
|
+
|
|
|
|
+ <div class="w-full mb-[18px]">
|
|
|
|
+ <el-form-item
|
|
|
|
+ v-if="formData.triggerMode == '2' || formData.triggerMode == '3'"
|
|
|
|
+ :label="t('marketingConfig.prompt')"
|
|
|
|
+ prop="promptMsg"
|
|
|
|
+ class="w-1/3"
|
|
|
|
+ >
|
|
|
|
+ <el-input v-model="formData.promptMsg" type="text" :placeholder="t('marketingConfig.promptTip')"></el-input>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item
|
|
|
|
+ v-if="formData.triggerMode == '1' || formData.triggerMode == '3'"
|
|
|
|
+ :label="t('marketingConfig.jumpLink')"
|
|
|
|
+ prop="url"
|
|
|
|
+ class="w-1/3"
|
|
|
|
+ >
|
|
|
|
+ <el-input v-model="formData.url" type="text" :placeholder="t('marketingConfig.jumpLinkTip')"></el-input>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="w-full">
|
|
|
|
+ <el-button type="primary" @click="onSubmit(ruleFormRef)" class="w-[80px] ml-5">{{ t('common.saveBtn') }}</el-button>
|
|
|
|
+ </div>
|
|
|
|
+ </el-form>
|
|
|
|
+ </div>
|
|
|
|
+ <template #footer>
|
|
|
|
+ <span class="dialog-footer">
|
|
|
|
+ <el-button @click="onCancel">{{ t('common.cancelButtonText') }}</el-button>
|
|
|
|
+ <el-button type="primary" @click="onSubmit" :disabled="loading">{{ t('common.confirmButtonText') }}</el-button>
|
|
|
|
+ </span>
|
|
|
|
+ </template>
|
|
|
|
+ </el-dialog>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script setup name="Edit" lang="ts">
|
|
|
|
+// 定义子组件向父组件传值/事件
|
|
|
|
+const emit = defineEmits(['update:open', 'onsuccess']);
|
|
|
|
+
|
|
|
|
+const props = defineProps({
|
|
|
|
+ open: {
|
|
|
|
+ type: Boolean,
|
|
|
|
+ default: false,
|
|
|
|
+ },
|
|
|
|
+ rowData: {
|
|
|
|
+ type: Array,
|
|
|
|
+ default: () => [],
|
|
|
|
+ },
|
|
|
|
+});
|
|
|
|
+const rowData = ref({});
|
|
|
|
+const onCancel = () => {
|
|
|
|
+ emit('update:open', false);
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+watch(
|
|
|
|
+ () => props.open,
|
|
|
|
+ (val) => {
|
|
|
|
+ if (val) {
|
|
|
|
+ rowData.value = props.rowData;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+);
|
|
|
|
+import { pageListIp, getConfigIpList, getConfigDomainList, getGroupDetail, getConfigDetail, saveConfigDetail } from '/@/api/marketing/config';
|
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
|
+import { useMessage } from '/@/hooks/message';
|
|
|
|
+import { rule } from '/@/utils/validate';
|
|
|
|
+import { ipSplicing } from '/@/utils/ipUpdate';
|
|
|
|
+
|
|
|
|
+// 引入组件
|
|
|
|
+const JCollapse = defineAsyncComponent(() => import('/@/components/JCollapse/index.vue'));
|
|
|
|
+const JDictSelect = defineAsyncComponent(() => import('/@/components/JDictSelect/index.vue'));
|
|
|
|
+const { t } = useI18n();
|
|
|
|
+// 定义变量内容
|
|
|
|
+
|
|
|
|
+//关闭或打开tabs的关闭按钮
|
|
|
|
+const closeDomainTags = ref(false);
|
|
|
|
+const closeIpTags = ref(false);
|
|
|
|
+
|
|
|
|
+// 弹窗
|
|
|
|
+const domainEditOpen = ref(false);
|
|
|
|
+const listEditOpen = ref(false);
|
|
|
|
+const delOpen = ref(false);
|
|
|
|
+const loading = ref(false);
|
|
|
|
+const ipActiveId = ref([]);
|
|
|
|
+const ipData = ref([]);
|
|
|
|
+const delObj = ref({});
|
|
|
|
+const ipWhiteList = ref([]); // ip白名单
|
|
|
|
+const ipBlackList = ref([]); // ip黑名单
|
|
|
|
+const domainList = ref([]);
|
|
|
|
+const ruleFormRef = ref();
|
|
|
|
+const formData = ref({
|
|
|
|
+ promptMsg: '',
|
|
|
|
+ triggerMode: '',
|
|
|
|
+ triggerRule: '',
|
|
|
|
+ url: '',
|
|
|
|
+ triggerNum: '',
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+const onLoadDetail = async (item: any) => {
|
|
|
|
+ if (item.list.length !== 0) return;
|
|
|
|
+ await getGroupDetail({ id: item.groupId }).then((val) => {
|
|
|
|
+ item.list = val.data.domains.map((item) => {
|
|
|
|
+ return item;
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+// // 表单校验规则
|
|
|
|
+const dataRules = reactive({
|
|
|
|
+ url: [
|
|
|
|
+ { required: true, message: '跳转连接不能为空', trigger: 'blur' },
|
|
|
|
+ { validator: rule.domain, trigger: 'blur' },
|
|
|
|
+ ],
|
|
|
|
+ promptMsg: [{ required: true, message: '提示信息不能为空', trigger: 'blur' }],
|
|
|
|
+ triggerNum: [
|
|
|
|
+ { required: true, message: '触发频率不能为空', trigger: 'blur' },
|
|
|
|
+ {
|
|
|
|
+ pattern: /^(10000|[1-9]\d{0,3}|0)$|^(100%|[1-9]?\d%|0%)$/,
|
|
|
|
+ message: '请输入 0-10000 的正整数或 0%-100% 的百分比',
|
|
|
|
+ trigger: 'blur',
|
|
|
|
+ },
|
|
|
|
+ ],
|
|
|
|
+});
|
|
|
|
+
|
|
|
|
+//tab相关
|
|
|
|
+
|
|
|
|
+const inputValue = ref('');
|
|
|
|
+const dynamicTags = ref(['关键字1', '关键字2', '关键字3']);//关键字数组
|
|
|
|
+const inputVisible = ref(false);
|
|
|
|
+const InputRef = ref();
|
|
|
|
+
|
|
|
|
+//删除关键字
|
|
|
|
+const handleClose = (tag: string) => {
|
|
|
|
+ dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1);
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const showInput = () => {
|
|
|
|
+ inputVisible.value = true;
|
|
|
|
+ nextTick(() => {
|
|
|
|
+ InputRef.value!.input!.focus();
|
|
|
|
+ });
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const handleInputConfirm = () => {
|
|
|
|
+ if (inputValue.value) {
|
|
|
|
+ dynamicTags.value.push(inputValue.value);
|
|
|
|
+ }
|
|
|
|
+ inputVisible.value = false;
|
|
|
|
+ inputValue.value = '';
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const onSubmit = async () => {
|
|
|
|
+ try {
|
|
|
|
+ await ruleFormRef.value.validateField('triggerNum');
|
|
|
|
+ } catch (error) {
|
|
|
|
+ // 验证失败,阻止后续逻辑执行
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (formData.value.triggerMode === '1' || formData.value.triggerMode === '3') {
|
|
|
|
+ try {
|
|
|
|
+ await ruleFormRef.value.validateField('url');
|
|
|
|
+ } catch (error) {
|
|
|
|
+ // 验证失败,阻止后续逻辑执行
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ loading.value = true;
|
|
|
|
+
|
|
|
|
+ // 处理 triggerNum 参数
|
|
|
|
+ let triggerNum = formData.value.triggerNum;
|
|
|
|
+ if (typeof triggerNum === 'string' && triggerNum.includes('%')) {
|
|
|
|
+ // 如果包含百分比符号,去除百分比符号并除以100
|
|
|
|
+ const num = parseFloat(triggerNum.replace('%', ''));
|
|
|
|
+ if (!isNaN(num)) {
|
|
|
|
+ triggerNum = num / 100;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ await saveConfigDetail({
|
|
|
|
+ ...formData.value,
|
|
|
|
+ triggerNum: triggerNum.toString(),
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ useMessage().success(t('common.editSuccessText'));
|
|
|
|
+ } catch (err) {
|
|
|
|
+ useMessage().error(err.msg);
|
|
|
|
+ } finally {
|
|
|
|
+ loading.value = false;
|
|
|
|
+ getConfig();
|
|
|
|
+ }
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const handleDelete = async (item: any, type: string) => {
|
|
|
|
+ console.log(item, type);
|
|
|
|
+ delObj.value = { ...item, delListType: type };
|
|
|
|
+ delOpen.value = true;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const getConfig = () => {
|
|
|
|
+ configIp();
|
|
|
|
+ configDomain();
|
|
|
|
+};
|
|
|
|
+const configDomain = async () => {
|
|
|
|
+ await getConfigDomainList().then((val) => {
|
|
|
|
+ domainList.value = val.data.map((item) => {
|
|
|
|
+ return {
|
|
|
|
+ ...item,
|
|
|
|
+ list: [],
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+};
|
|
|
|
+const configIp = async () => {
|
|
|
|
+ await getConfigIpList().then((val) => {
|
|
|
|
+ ipWhiteList.value = val.data
|
|
|
|
+ .filter((item) => item.ipType === 1)
|
|
|
|
+ .map((item) => {
|
|
|
|
+ return { ...item, list: [] };
|
|
|
|
+ });
|
|
|
|
+ ipBlackList.value = val.data
|
|
|
|
+ .filter((item) => item.ipType === 2)
|
|
|
|
+ .map((item) => {
|
|
|
|
+ return { ...item, list: [] };
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+};
|
|
|
|
+const getIpData = async () => {
|
|
|
|
+ await pageListIp().then((val) => {
|
|
|
|
+ ipActiveId.value = [];
|
|
|
|
+ ipData.value = val.data.map((item: any) => {
|
|
|
|
+ ipActiveId.value.push(item.id);
|
|
|
|
+ return {
|
|
|
|
+ ...item,
|
|
|
|
+ title: item.groupName,
|
|
|
|
+ id: item.id,
|
|
|
|
+ list: item.ips.map((items: any) => {
|
|
|
|
+ return {
|
|
|
|
+ ...items,
|
|
|
|
+ id: items.id,
|
|
|
|
+ value: ipSplicing(items.startIp, items.endIp),
|
|
|
|
+ };
|
|
|
|
+ }),
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ await getConfigDetail().then((val) => {
|
|
|
|
+ formData.value = {
|
|
|
|
+ promptMsg: '',
|
|
|
|
+ triggerMode: '',
|
|
|
|
+ triggerRule: '',
|
|
|
|
+ url: '',
|
|
|
|
+ triggerNum: '',
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ formData.value = {
|
|
|
|
+ ...val.data,
|
|
|
|
+ triggerMode: val.data?.triggerMode.toString(),
|
|
|
|
+ triggerRule: val.data?.triggerRule.toString(),
|
|
|
|
+ triggerNum: formatNum(val.data?.triggerNum),
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+};
|
|
|
|
+// 格式化数据展示
|
|
|
|
+const formatNum = (value: string | number = 0) => {
|
|
|
|
+ let num = Number(value);
|
|
|
|
+ if (num > 0 && num < 1) {
|
|
|
|
+ return (num * 100).toFixed(0) + '%';
|
|
|
|
+ } else if (num >= 1 && num < 10000) {
|
|
|
|
+ return num;
|
|
|
|
+ }
|
|
|
|
+ return '--';
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+onMounted(() => {
|
|
|
|
+ //获取IP列表
|
|
|
|
+ getIpData();
|
|
|
|
+ getConfig();
|
|
|
|
+});
|
|
|
|
+</script>
|
|
|
|
+<style lang="scss">
|
|
|
|
+</style>
|