|
@@ -1,202 +1,105 @@
|
|
|
<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-57vw]">
|
|
|
- <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>
|
|
|
+ <el-dialog :title="props.rowData?.id ? '修改规则' : '新增规则'" width="1000" v-model="props.open"
|
|
|
+ :close-on-click-modal="false" :destroy-on-close="true" @close="onCancel" draggable>
|
|
|
+ <div class="w-full ml-[-8px]">
|
|
|
+ <el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px" class="flex flex-wrap">
|
|
|
+ <el-form-item label="规则名称" prop="ruleName" class="w-1/3">
|
|
|
+ <el-input v-model="formData.ruleName" type="text" placeholder="请输入规则名称" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div class="px-4 rounded overflow-y-auto mt-4" style="max-height: calc(100vh - 350px)">
|
|
|
+ <div class="flex items-start">
|
|
|
+ <label class="w-[65px] leading-8 text-right"><span class="text-[#f56c6c] mr-1">*</span> 关键字</label>
|
|
|
+ <div class="flex gap-2 ml-2 flex-wrap w-57vw]">
|
|
|
+ <el-tag v-for="tag in keywordData" :key="tag" size="large" closable :disable-transitions="false"
|
|
|
+ @close="handleClose(tag, 'keyword')">
|
|
|
+ {{ tag }}
|
|
|
+ </el-tag>
|
|
|
+ <el-input v-if="inputVisible" ref="InputRef" v-model="inputValue" class="!w-32"
|
|
|
+ @keyup.enter="handleInputConfirm('keyword')" @blur="handleInputConfirm('keyword')" />
|
|
|
+ <el-button v-else class="button-new-tag" @click="showInput"> 添加关键字</el-button>
|
|
|
+ <!-- 注册隐藏的表单项以启用校验 -->
|
|
|
+ <el-form-item prop="keyword" style="display:none;"></el-form-item>
|
|
|
+ </div>
|
|
|
</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 class="px-4 rounded overflow-y-auto mt-4" style="max-height: calc(100vh - 350px)">
|
|
|
+ <div class="flex items-start">
|
|
|
+ <label class="w-[65px] leading-8 text-right">IP</label>
|
|
|
+ <div class="flex gap-2 ml-2 flex-wrap w-57vw]">
|
|
|
+ <el-tag v-for="tag in ipData" :key="tag" size="large" closable :disable-transitions="false"
|
|
|
+ @close="handleClose(tag, 'ip')">
|
|
|
+ {{ tag }}
|
|
|
+ </el-tag>
|
|
|
+ <el-input v-if="ipInputVisible" ref="ipInputRef" v-model="ipInputValue" class="!w-32"
|
|
|
+ @keyup.enter="handleInputConfirm('ip')" @blur="handleInputConfirm('ip')" />
|
|
|
+ <el-button v-else class="button-new-tag" @click="showIpInput"> 添加IP</el-button>
|
|
|
+ <!-- 注册隐藏的表单项以启用校验 -->
|
|
|
+ <el-form-item prop="ip" style="display:none;"></el-form-item>
|
|
|
</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>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="px-4 rounded overflow-y-auto mt-4" style="max-height: calc(100vh - 350px)">
|
|
|
+ <div class="flex items-start">
|
|
|
+ <label class="w-[66px] leading-8 text-right">添加域名</label>
|
|
|
+ <div class="flex gap-2 ml-2 flex-wrap w-57vw]">
|
|
|
+ <el-tag v-for="tag in domainData" :key="tag" size="large" closable :disable-transitions="false"
|
|
|
+ @close="handleClose(tag, 'domain')">
|
|
|
+ {{ tag }}
|
|
|
+ </el-tag>
|
|
|
+ <el-input v-if="domainInputVisible" ref="domainInputRef" v-model="domainInputValue"
|
|
|
+ class="!w-32" @keyup.enter="handleInputConfirm('domain')"
|
|
|
+ @blur="handleInputConfirm('domain')" placeholder="如: example.com" />
|
|
|
+ <el-button v-else class="button-new-tag" @click="showDomainInput"> 添加域名</el-button>
|
|
|
+ <!-- 注册隐藏的表单项以启用校验 -->
|
|
|
+ <el-form-item prop="domain" style="display:none;"></el-form-item>
|
|
|
</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'"
|
|
|
- />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px"
|
|
|
+ class="flex flex-wrap mt-4">
|
|
|
+ <el-form-item label="推送方式" prop="action" class="w-1/2">
|
|
|
+ <JDictSelect v-model:value="formData.action" placeholder="请选择推送方式" :dictType="'pushMode'"
|
|
|
+ :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 label="推送频率" prop="pushFrequency" class="w-1/2">
|
|
|
+ <el-input v-model="formData.pushFrequency" type="text" placeholder="请输入推送频率" />
|
|
|
</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 label="推送图片" prop="pushContent" class="w-1/2 ">
|
|
|
+ <div class="flex items-start">
|
|
|
+ <el-switch v-model="formData.pushType" class="mr-2"
|
|
|
+ @change="oldUrl = formData.pushContent, formData.pushContent = ''" />
|
|
|
+ <Image v-if="formData.pushType" @change="success" :imageUrl="formData.pushContent"
|
|
|
+ @update="success" />
|
|
|
+ </div>
|
|
|
</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-item v-if="!formData.pushType" label="推送内容" prop="pushContent" class="w-full">
|
|
|
+ <el-input v-model="formData.pushContent" type="textarea" placeholder="请输入推送内容" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注" prop="remark" class="w-full">
|
|
|
+ <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
|
|
|
+ </el-form-item>
|
|
|
</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>
|
|
|
+ <el-button type="primary" @click="onSubmit"
|
|
|
+ :disabled="loading">{{ t('common.confirmButtonText') }}</el-button>
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="Edit" lang="ts">
|
|
|
+import { update } from '/@/api/marketing/rules';
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
+import { useMessage } from '/@/hooks/message';
|
|
|
+let baseURL = import.meta.env.VITE_API_URL;
|
|
|
+
|
|
|
// 定义子组件向父组件传值/事件
|
|
|
const emit = defineEmits(['update:open', 'onsuccess']);
|
|
|
|
|
@@ -206,77 +109,94 @@ const props = defineProps({
|
|
|
default: false,
|
|
|
},
|
|
|
rowData: {
|
|
|
- type: Array,
|
|
|
- default: () => [],
|
|
|
+ type: Object,
|
|
|
+ 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 oldUrl = ref('');
|
|
|
+const success = (val: string) => {
|
|
|
+ formData.value.pushContent = val;
|
|
|
+};
|
|
|
|
|
|
// 引入组件
|
|
|
-const JCollapse = defineAsyncComponent(() => import('/@/components/JCollapse/index.vue'));
|
|
|
const JDictSelect = defineAsyncComponent(() => import('/@/components/JDictSelect/index.vue'));
|
|
|
+const Image = defineAsyncComponent(() => import('/@/components/Upload/Image.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: '',
|
|
|
-});
|
|
|
+interface FormDataType {
|
|
|
+ ruleName: string;
|
|
|
+ keyword: string[];
|
|
|
+ ip: string[];
|
|
|
+ domain: string[];
|
|
|
+ pushContent: string;
|
|
|
+ pushFrequency: string;
|
|
|
+ action: string;
|
|
|
+ remark: string;
|
|
|
+ pushType: boolean;
|
|
|
+}
|
|
|
|
|
|
-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 formData = ref<FormDataType>({
|
|
|
+ ruleName: '',
|
|
|
+ keyword: [],
|
|
|
+ ip: [],
|
|
|
+ domain: [],
|
|
|
+ pushContent: '',
|
|
|
+ pushFrequency: '',
|
|
|
+ action: '1',
|
|
|
+ remark: '',
|
|
|
+ pushType: false,
|
|
|
+});
|
|
|
|
|
|
// // 表单校验规则
|
|
|
const dataRules = reactive({
|
|
|
- url: [
|
|
|
- { required: true, message: '跳转连接不能为空', trigger: 'blur' },
|
|
|
- { validator: rule.domain, trigger: 'blur' },
|
|
|
+ ruleName: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
|
|
|
+ keyword: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ validator: (rule: any, value: any, callback: any) => {
|
|
|
+ if (!keywordData.value || keywordData.value.length === 0) {
|
|
|
+ callback(new Error('关键字不能为空'));
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ pushContent: [{ required: true, message: '推送内容不能为空', trigger: 'blur' }],
|
|
|
+ ip: [
|
|
|
+ {
|
|
|
+ validator: (rule: any, value: any, callback: any) => {
|
|
|
+ if (!ipData.value || ipData.value.length === 0) {
|
|
|
+ callback(new Error('IP不能为空'));
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ domain: [
|
|
|
+ {
|
|
|
+ validator: (rule: any, value: any, callback: any) => {
|
|
|
+ if (!domainData.value || domainData.value.length === 0) {
|
|
|
+ callback(new Error('域名不能为空'));
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur',
|
|
|
+ },
|
|
|
],
|
|
|
- promptMsg: [{ required: true, message: '提示信息不能为空', trigger: 'blur' }],
|
|
|
- triggerNum: [
|
|
|
- { required: true, message: '触发频率不能为空', trigger: 'blur' },
|
|
|
+ pushFrequency: [
|
|
|
+ { required: true, message: '推送频率不能为空', trigger: 'blur' },
|
|
|
{
|
|
|
pattern: /^(10000|[1-9]\d{0,3}|0)$|^(100%|[1-9]?\d%|0%)$/,
|
|
|
message: '请输入 0-10000 的正整数或 0%-100% 的百分比',
|
|
@@ -288,14 +208,20 @@ const dataRules = reactive({
|
|
|
//tab相关
|
|
|
|
|
|
const inputValue = ref('');
|
|
|
-const dynamicTags = ref(['关键字1', '关键字2', '关键字3']);//关键字数组
|
|
|
+const ipInputValue = ref('');
|
|
|
+const domainInputValue = ref('');
|
|
|
+
|
|
|
const inputVisible = ref(false);
|
|
|
+const ipInputVisible = ref(false);
|
|
|
+const domainInputVisible = ref(false);
|
|
|
+
|
|
|
const InputRef = ref();
|
|
|
+const ipInputRef = ref();
|
|
|
+const domainInputRef = ref();
|
|
|
|
|
|
-//删除关键字
|
|
|
-const handleClose = (tag: string) => {
|
|
|
- dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1);
|
|
|
-};
|
|
|
+const keywordData = ref<string[]>([]);
|
|
|
+const ipData = ref<string[]>([]);
|
|
|
+const domainData = ref<string[]>([]);
|
|
|
|
|
|
const showInput = () => {
|
|
|
inputVisible.value = true;
|
|
@@ -303,146 +229,270 @@ const showInput = () => {
|
|
|
InputRef.value!.input!.focus();
|
|
|
});
|
|
|
};
|
|
|
+const showIpInput = () => {
|
|
|
+ ipInputVisible.value = true;
|
|
|
+ nextTick(() => {
|
|
|
+ ipInputRef.value!.input!.focus();
|
|
|
+ });
|
|
|
+};
|
|
|
+const showDomainInput = () => {
|
|
|
+ domainInputVisible.value = true;
|
|
|
+ nextTick(() => {
|
|
|
+ domainInputRef.value!.input!.focus();
|
|
|
+ });
|
|
|
+};
|
|
|
+const handleClose = (tag: string, type: string) => {
|
|
|
+ if (type === 'keyword') {
|
|
|
+ keywordData.value = keywordData.value.filter((item: string) => item !== tag);
|
|
|
+ } else if (type === 'ip') {
|
|
|
+ ipData.value = ipData.value.filter((item: string) => item !== tag);
|
|
|
+ } else if (type === 'domain') {
|
|
|
+ domainData.value = domainData.value.filter((item: string) => item !== tag);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 自定义校验函数
|
|
|
+const validateIP = (ip: string): boolean => {
|
|
|
+ // 支持IP段格式:192.168.3.4/255
|
|
|
+ if (ip.includes('/')) {
|
|
|
+ const [ipPart, rangePart] = ip.split('/');
|
|
|
+ const range = parseInt(rangePart);
|
|
|
+
|
|
|
+ // 验证IP部分
|
|
|
+ const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
|
+ if (!ipRegex.test(ipPart)) return false;
|
|
|
|
|
|
-const handleInputConfirm = () => {
|
|
|
- if (inputValue.value) {
|
|
|
- dynamicTags.value.push(inputValue.value);
|
|
|
+ // 验证范围部分(1-255)
|
|
|
+ if (isNaN(range) || range < 1 || range > 255) return false;
|
|
|
+
|
|
|
+ // 验证IP部分的每个段
|
|
|
+ const parts = ipPart.split('.');
|
|
|
+ return parts.every((part) => {
|
|
|
+ const num = parseInt(part);
|
|
|
+ return num >= 0 && num <= 255;
|
|
|
+ });
|
|
|
}
|
|
|
- inputVisible.value = false;
|
|
|
- inputValue.value = '';
|
|
|
+
|
|
|
+ // 普通IP格式
|
|
|
+ const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
|
+ if (!ipRegex.test(ip)) return false;
|
|
|
+
|
|
|
+ const parts = ip.split('.');
|
|
|
+ return parts.every((part) => {
|
|
|
+ const num = parseInt(part);
|
|
|
+ return num >= 0 && num <= 255;
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-const onSubmit = async () => {
|
|
|
- try {
|
|
|
- await ruleFormRef.value.validateField('triggerNum');
|
|
|
- } catch (error) {
|
|
|
- // 验证失败,阻止后续逻辑执行
|
|
|
- return;
|
|
|
+// 检查IP是否在某个IP段内
|
|
|
+const isIPInRange = (ip: string, ipRange: string): boolean => {
|
|
|
+ if (!ipRange.includes('/')) return false;
|
|
|
+
|
|
|
+ const [rangeIP, rangePart] = ipRange.split('/');
|
|
|
+ const range = parseInt(rangePart);
|
|
|
+
|
|
|
+ // 将IP转换为数字进行比较
|
|
|
+ const ipToNumber = (ipStr: string): number => {
|
|
|
+ const parts = ipStr.split('.').map((part) => parseInt(part));
|
|
|
+ return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
|
|
+ };
|
|
|
+
|
|
|
+ const ipNum = ipToNumber(ip);
|
|
|
+ const rangeIPNum = ipToNumber(rangeIP);
|
|
|
+ const rangeEndNum = rangeIPNum + range - 1;
|
|
|
+
|
|
|
+ return ipNum >= rangeIPNum && ipNum <= rangeEndNum;
|
|
|
+};
|
|
|
+
|
|
|
+// 检查IP是否与现有IP段冲突
|
|
|
+const isIPConflict = (newIP: string): boolean => {
|
|
|
+ // 如果新添加的是IP段,检查是否与现有IP冲突
|
|
|
+ if (newIP.includes('/')) {
|
|
|
+ const [rangeIP, rangePart] = newIP.split('/');
|
|
|
+ const range = parseInt(rangePart);
|
|
|
+ const rangeIPNum = ipToNumber(rangeIP);
|
|
|
+ const rangeEndNum = rangeIPNum + range - 1;
|
|
|
+
|
|
|
+ // 检查现有IP是否在新IP段内
|
|
|
+ for (const existingIP of ipData.value) {
|
|
|
+ if (!existingIP.includes('/')) {
|
|
|
+ const existingIPNum = ipToNumber(existingIP);
|
|
|
+ if (existingIPNum >= rangeIPNum && existingIPNum <= rangeEndNum) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果新添加的是单个IP,检查是否在现有IP段内
|
|
|
+ for (const existingIP of ipData.value) {
|
|
|
+ if (existingIP.includes('/') && isIPInRange(newIP, existingIP)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+// 辅助函数:将IP转换为数字
|
|
|
+const ipToNumber = (ipStr: string): number => {
|
|
|
+ const parts = ipStr.split('.').map((part) => parseInt(part));
|
|
|
+ return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
|
|
+};
|
|
|
+
|
|
|
+const validateDomain = (domain: string): boolean => {
|
|
|
+ // 移除首尾空格
|
|
|
+ const trimmedDomain = domain.trim();
|
|
|
+
|
|
|
+ // 基本长度检查
|
|
|
+ if (trimmedDomain.length === 0 || trimmedDomain.length > 253) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否包含至少一个点号(顶级域名)
|
|
|
+ if (!trimmedDomain.includes('.')) {
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
- if (formData.value.triggerMode === '1' || formData.value.triggerMode === '3') {
|
|
|
- try {
|
|
|
- await ruleFormRef.value.validateField('url');
|
|
|
- } catch (error) {
|
|
|
- // 验证失败,阻止后续逻辑执行
|
|
|
- return;
|
|
|
+ // 分割域名部分
|
|
|
+ const parts = trimmedDomain.split('.');
|
|
|
+
|
|
|
+ // 检查域名部分数量(至少2部分:主域名.顶级域名)
|
|
|
+ if (parts.length < 2) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查每个部分
|
|
|
+ for (let i = 0; i < parts.length; i++) {
|
|
|
+ const part = parts[i];
|
|
|
+
|
|
|
+ // 每个部分不能为空
|
|
|
+ if (part.length === 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 顶级域名至少2个字符
|
|
|
+ if (i === parts.length - 1 && part.length < 2) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 每个部分不能超过63个字符
|
|
|
+ if (part.length > 63) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查字符格式:只能包含字母、数字、连字符,不能以连字符开头或结尾
|
|
|
+ const partRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
|
+ if (!partRegex.test(part)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 顶级域名只能包含字母
|
|
|
+ if (i === parts.length - 1) {
|
|
|
+ const tldRegex = /^[a-zA-Z]+$/;
|
|
|
+ if (!tldRegex.test(part)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- try {
|
|
|
- loading.value = true;
|
|
|
+ return 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;
|
|
|
+const handleInputConfirm = (type: string) => {
|
|
|
+ if (type === 'keyword') {
|
|
|
+ if (inputValue.value) {
|
|
|
+ keywordData.value.push(inputValue.value);
|
|
|
+ }
|
|
|
+ inputVisible.value = false;
|
|
|
+ inputValue.value = '';
|
|
|
+ } else if (type === 'ip') {
|
|
|
+ if (ipInputValue.value) {
|
|
|
+ // 校验IP格式
|
|
|
+ if (validateIP(ipInputValue.value)) {
|
|
|
+ // 检查IP冲突
|
|
|
+ if (isIPConflict(ipInputValue.value)) {
|
|
|
+ useMessage().error('该IP与现有IP段冲突,无法添加');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ipData.value.push(ipInputValue.value);
|
|
|
+ } else {
|
|
|
+ useMessage().error('请输入正确的IP地址格式,支持单个IP(192.168.1.1)或IP段(192.168.1.1/255)');
|
|
|
+ return;
|
|
|
}
|
|
|
}
|
|
|
+ ipInputVisible.value = false;
|
|
|
+ ipInputValue.value = '';
|
|
|
+ } else if (type === 'domain') {
|
|
|
+ if (domainInputValue.value) {
|
|
|
+ // 校验域名格式
|
|
|
+ if (validateDomain(domainInputValue.value)) {
|
|
|
+ domainData.value.push(domainInputValue.value);
|
|
|
+ } else {
|
|
|
+ useMessage().error('请输入正确的域名格式,例如:example.com、sub.example.com');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ domainInputVisible.value = false;
|
|
|
+ domainInputValue.value = '';
|
|
|
+ }
|
|
|
+};
|
|
|
+const getpushContent = () => {
|
|
|
+ if (formData.value.pushContent) {
|
|
|
+ return formData.value.pushContent.includes('http') ? formData.value.pushContent : baseURL + formData.value.pushContent
|
|
|
+ } else {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onSubmit = async () => {
|
|
|
+ // 触发表单校验(包含隐藏注册的 keyword/ip/domain 规则)
|
|
|
+ try {
|
|
|
+ await ruleFormRef.value.validate();
|
|
|
+ } catch (e) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ formData.value = {
|
|
|
+ ...formData.value,
|
|
|
+ ip: ipData.value,
|
|
|
+ keyword: keywordData.value,
|
|
|
+ domain: domainData.value,
|
|
|
+ pushContent: getpushContent(),
|
|
|
+ };
|
|
|
+ if (!formData.value.pushContent) {
|
|
|
+ return useMessage().error(formData.value.pushType ? '推送图片不能为空' : '推送内容不能为空');
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ loading.value = true;
|
|
|
|
|
|
- await saveConfigDetail({
|
|
|
+ await update({
|
|
|
...formData.value,
|
|
|
- triggerNum: triggerNum.toString(),
|
|
|
});
|
|
|
|
|
|
- useMessage().success(t('common.editSuccessText'));
|
|
|
- } catch (err) {
|
|
|
+ useMessage().success(props.rowData?.id ? t('common.editSuccessText') : t('common.addSuccessText'));
|
|
|
+ emit('onsuccess');
|
|
|
+ onCancel();
|
|
|
+ } catch (err: any) {
|
|
|
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),
|
|
|
- };
|
|
|
- }),
|
|
|
+watch(
|
|
|
+ () => props.open,
|
|
|
+ (val) => {
|
|
|
+ if (val) {
|
|
|
+ formData.value = {
|
|
|
+ ...props.rowData,
|
|
|
};
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- 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;
|
|
|
+ ipData.value = props.rowData.ip || [];
|
|
|
+ keywordData.value = props.rowData.keyword || [];
|
|
|
+ domainData.value = props.rowData.domain || [];
|
|
|
+ oldUrl.value = props.rowData.pushContent;
|
|
|
+ }
|
|
|
}
|
|
|
- return '--';
|
|
|
-};
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- //获取IP列表
|
|
|
- getIpData();
|
|
|
- getConfig();
|
|
|
-});
|
|
|
+);
|
|
|
</script>
|
|
|
<style lang="scss">
|
|
|
</style>
|