浏览代码

Merge branch 'dev-jcq' of https://s-20coaj910c.zht2.com/cmy/data-marketing-platform into dev-ly

luoy 1 天之前
父节点
当前提交
24050632e7

+ 17 - 0
src/api/marketing/config.ts

@@ -117,6 +117,23 @@ export const addGroup = (data: object) => {
 		data: data
 	});
 };
+//获取主动推送
+export const getRule = (params?: Object) => {
+	return request({
+		url: '/marketing/config/getRule',
+		method: 'get',
+		params,
+	});
+};
+//保存主动推送
+export const saveRule = (data?: Object) => {
+	return request({
+		url: '/marketing/config/setRule',
+		method: 'post',
+		data,
+	});
+};
+
 
 
 /**

+ 457 - 0
src/views/marketing/config/components/push.vue

@@ -0,0 +1,457 @@
+<template>
+	<div class="w-full ml-[-8px] mt-5">
+		<div class="px-4 rounded overflow-y-auto w-full" 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>
+		</div>
+
+		<div class="px-4 rounded overflow-y-auto mt-4 w-full" 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>
+			</div>
+		</div>
+		<el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px" class="flex flex-wrap mt-4 w-1/2">
+			<el-form-item label="主动推送" prop="autoPush" class="w-full">
+				<el-switch v-model="formData.autoPush" class="mr-2" />
+			</el-form-item>
+			<el-form-item label="推送方式" prop="action" class="w-1/2">
+				<JDictSelect v-model:value="formData.action" placeholder="请选择推送方式" dictType="pushMode" :styleClass="'w-full'" />
+			</el-form-item>
+
+			<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="推送延时" prop="delayPush" class="w-1/2">
+				<div class="flex items-start w-full">
+					<el-input v-model="formData.delayPush" type="text" placeholder="请输入推送延时" />
+				</div>
+			</el-form-item>
+			<el-form-item label="推送图片" prop="pushContent" class="w-full">
+				<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>
+
+			<el-form-item v-if="!formData.pushType" label="推送内容" prop="pushContent" class="w-full">
+				<el-input v-model="formData.pushContent" type="text" placeholder="请输入推送内容" />
+			</el-form-item>
+		</el-form>
+		<el-button type="primary" class="ml-5 mt-4 w-[80px]" @click="onSubmit" :disabled="loading">{{ t('common.saveBtn') }}</el-button>
+	</div>
+</template>
+
+<script setup name="Edit" lang="ts">
+import { saveRule } from '/@/api/marketing/config';
+import { useI18n } from 'vue-i18n';
+import { useMessage } from '/@/hooks/message';
+let baseURL = import.meta.env.VITE_API_URL;
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['update:open', 'onsuccess']);
+
+const props = defineProps({
+	open: {
+		type: Boolean,
+		default: false,
+	},
+	rowData: {
+		type: Object,
+		default: () => {},
+	},
+});
+
+const oldUrl = ref('');
+const success = (val: string) => {
+	formData.value.pushContent = val;
+};
+
+// 引入组件
+const JDictSelect = defineAsyncComponent(() => import('/@/components/JDictSelect/index.vue'));
+const Image = defineAsyncComponent(() => import('/@/components/Upload/Image.vue'));
+const { t } = useI18n();
+// 定义变量内容
+
+const loading = ref(false);
+
+const ruleFormRef = ref();
+
+const formData = ref<any>({
+	ruleName: '',
+	keyword: [],
+	ip: [],
+	domain: [],
+	pushContent: '',
+	pushFrequency: '',
+	action: '1',
+	pushType: false,
+	delayPush: '',
+	autoPush: false,
+});
+
+// // 表单校验规则
+const dataRules = reactive({
+	ruleName: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
+	delayPush: [{ required: true, message: '推送延时不能为空', 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',
+		},
+	],
+	pushFrequency: [
+		{ 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 ipInputValue = ref('');
+const domainInputValue = ref('');
+
+const ipInputVisible = ref(false);
+const domainInputVisible = ref(false);
+
+const ipInputRef = ref();
+const domainInputRef = ref();
+
+const ipData = ref<string[]>([]);
+const domainData = ref<string[]>([]);
+
+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 === '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;
+
+		// 验证范围部分(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;
+		});
+	}
+
+	// 普通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;
+	});
+};
+
+// 检查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;
+	}
+
+	// 分割域名部分
+	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;
+			}
+		}
+	}
+
+	return true;
+};
+
+const handleInputConfirm = (type: string) => {
+	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 && formData.value.pushType) {
+		return formData.value.pushContent.includes('http') ? formData.value.pushContent : baseURL + formData.value.pushContent;
+	} else {
+		return formData.value.pushContent;
+	}
+};
+
+const onSubmit = async () => {
+	// 触发表单校验(包含隐藏注册的 keyword/ip/domain 规则)
+	try {
+		await ruleFormRef.value.validate();
+	} catch (e) {
+		return;
+	}
+	let pushFrequency = formData.value.pushFrequency;
+	if (typeof pushFrequency === 'string' && pushFrequency.includes('%')) {
+		// 如果包含百分比符号,去除百分比符号并除以100
+		const num = parseFloat(pushFrequency.replace('%', ''));
+		if (!isNaN(num)) {
+			pushFrequency = num / 100;
+		}
+	}
+	formData.value = {
+		...formData.value,
+		ip: ipData.value,
+		domain: domainData.value,
+		pushContent: getpushContent(),
+		pushType: formData.value.pushType || false,
+		
+	};
+
+	if (!formData.value.pushContent) {
+		return useMessage().error(formData.value.pushType ? '推送图片不能为空' : '推送内容不能为空');
+	}
+	try {
+		loading.value = true;
+
+		await saveRule({
+			...formData.value,
+            pushFrequency: pushFrequency.toString(),
+		});
+		useMessage().success(t('common.editSuccessText'));
+		emit('onsuccess');
+	} catch (err: any) {
+		useMessage().error(err.msg);
+	} finally {
+		loading.value = false;
+	}
+};
+// 格式化数据展示
+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 '--';
+};
+
+watch(
+	() => props.rowData,
+	(val) => {
+		if (val) {
+			formData.value = {
+				...props.rowData,
+				action: props.rowData?.action || '1',
+				pushFrequency: formatNum(props.rowData?.pushFrequency),
+			};
+			ipData.value = props.rowData.ip || [];
+			domainData.value = props.rowData.domain || [];
+			oldUrl.value = props.rowData.pushContent;
+		}
+	}
+);
+</script>
+<style lang="scss">
+</style>

+ 43 - 23
src/views/marketing/config/index.vue

@@ -4,10 +4,14 @@
 			<el-tab-pane :label="t('marketingConfig.ipList')" name="IP分组" class="layout-padding-auto layout-padding-view" :loading="loading">
 				<Title class="ml-4" :title="t('marketingConfig.ipList')" />
 				<div class="p-4 rounded">
-					<el-button style="margin-bottom: 10px;" type="primary" @click="onClickAdd('ip')">{{ t('marketingConfig.addIpList') }}</el-button><br>
-					筛选分组:<el-input placeholder="请输入分组名称" clearable
-						style="display: inline-block; width: 200px; margin-left: 5px;"
-						v-model="queryIPName" />
+					<el-button style="margin-bottom: 10px" type="primary" @click="onClickAdd('ip')">{{ t('marketingConfig.addIpList') }}</el-button
+					><br />
+					筛选分组:<el-input
+						placeholder="请输入分组名称"
+						clearable
+						style="display: inline-block; width: 200px; margin-left: 5px"
+						v-model="queryIPName"
+					/>
 					<div v-if="ipData.length > 0" class="overflow-y-auto mt-2" style="height: calc(100vh - 300px)">
 						<JCollapse
 							@update="(item) => onClickEdit(item, 'ip')"
@@ -23,10 +27,14 @@
 			<el-tab-pane :label="t('marketingConfig.domainList')" name="域名分组" class="layout-padding-auto layout-padding-view">
 				<Title class="ml-4" :title="t('marketingConfig.domainList')" />
 				<div class="p-4 rounded">
-					<el-button style="margin-bottom: 10px;" type="primary" @click="onClickAdd('domain')">{{ t('marketingConfig.addDomainList') }}</el-button><br>
-					筛选分组:<el-input placeholder="请输入分组名称" clearable
-					style="display: inline-block; width: 200px; margin-left: 5px;"
-					v-model="queryDomainName" />
+					<el-button style="margin-bottom: 10px" type="primary" @click="onClickAdd('domain')">{{ t('marketingConfig.addDomainList') }}</el-button
+					><br />
+					筛选分组:<el-input
+						placeholder="请输入分组名称"
+						clearable
+						style="display: inline-block; width: 200px; margin-left: 5px"
+						v-model="queryDomainName"
+					/>
 					<div v-if="domainData.length > 0" class="overflow-y-auto mt-2" style="height: calc(100vh - 300px)">
 						<JCollapse
 							@update="(item) => onClickEdit(item, 'domain')"
@@ -226,6 +234,10 @@
 					</el-form>
 				</div>
 			</el-tab-pane>
+			<el-tab-pane label="主动推送" name="主动推送" class="layout-padding-auto layout-padding-view">
+				<Title class="ml-4" title="主动推送" />
+				<Push :row-data="pushData" @onsuccess="getRuleList"/>
+			</el-tab-pane>
 		</el-tabs>
 		<DomainEdit :select-data="domainData" v-model:open="domainEditOpen" @onsuccess="getConfig" />
 		<ListEdit :select-data="ipData" v-model:open="listEditOpen" @onsuccess="getConfig" />
@@ -255,6 +267,7 @@ import {
 	getGroupDetail,
 	getConfigDetail,
 	saveConfigDetail,
+	getRule
 } from '/@/api/marketing/config';
 import { useI18n } from 'vue-i18n';
 import { useMessage } from '/@/hooks/message';
@@ -269,11 +282,13 @@ const ListEdit = defineAsyncComponent(() => import('./components/listEdit.vue'))
 const GroupingEdit = defineAsyncComponent(() => import('./components/ipGroupingEdit.vue'));
 const IpListEdit = defineAsyncComponent(() => import('./components/ipListEdit.vue'));
 const JDictSelect = defineAsyncComponent(() => import('/@/components/JDictSelect/index.vue'));
+const Push = defineAsyncComponent(() => import('./components/push.vue'));
 const { t } = useI18n();
 // 定义变量内容
 const activeName = ref('IP分组');
 const queryIPName = ref('');
 const queryDomainName = ref('');
+const pushData = ref({})
 const showIPData = computed(() => {
 	return ipData.value.filter((item) => item.groupName.includes(queryIPName.value));
 });
@@ -329,7 +344,6 @@ const onLoadDetail = async (item: any) => {
 			return item;
 		});
 	});
-	
 };
 
 // // 表单校验规则
@@ -338,9 +352,7 @@ const dataRules = reactive({
 		{ required: true, message: '跳转连接不能为空', trigger: 'blur' },
 		{ validator: rule.domain, trigger: 'blur' },
 	],
-	promptMsg: [
-		{ required: true, message: '提示信息不能为空', trigger: 'blur' },
-	],
+	promptMsg: [{ required: true, message: '提示信息不能为空', trigger: 'blur' }],
 	triggerNum: [
 		{ required: true, message: '触发频率不能为空', trigger: 'blur' },
 		{
@@ -403,10 +415,10 @@ const handleClick = (data: any) => {
 		getIpData();
 		getDomainData();
 		getConfig();
+		getRuleList()
 	}
 };
 const handleDelete = async (item: any, type: string) => {
-	console.log(item, type);
 	delObj.value = { ...item, delListType: type };
 	delOpen.value = true;
 };
@@ -535,7 +547,7 @@ const getIpData = async () => {
 			url: '',
 			triggerNum: '',
 		};
-		
+
 		formData.value = {
 			...val.data,
 			triggerMode: val.data?.triggerMode.toString(),
@@ -546,18 +558,26 @@ const getIpData = async () => {
 };
 // 格式化数据展示
 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 '--'
-}
-
+	let num = Number(value);
+	if (num > 0 && num < 1) {
+		return (num * 100).toFixed(0) + '%';
+	} else if (num >= 1 && num < 10000) {
+		return num;
+	}
+	return '--';
+};
+const getRuleList = async () => { 
+	const res = await getRule();
+	console.log(res);
+	const data = res.data;
+	pushData.value = data
+	
+};
 
 onMounted(() => {
 	//获取IP列表
+	getRuleList();
+
 	getIpData();
 	getConfig();
 });

+ 78 - 36
src/views/marketing/rules/components/Edit.vue

@@ -1,6 +1,13 @@
 <template>
-	<el-dialog :title="props.rowData?.id ? '修改规则' : '新增规则'" width="1000" v-model="props.open"
-		:close-on-click-modal="false" :destroy-on-close="true" @close="onCancel" draggable>
+	<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">
@@ -11,15 +18,20 @@
 				<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')">
+						<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-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>
+						<el-form-item prop="keyword" style="display: none"></el-form-item>
 					</div>
 				</div>
 			</div>
@@ -27,15 +39,20 @@
 				<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')">
+						<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-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>
+						<el-form-item prop="ip" style="display: none"></el-form-item>
 					</div>
 				</div>
 			</div>
@@ -44,40 +61,46 @@
 				<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')">
+						<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-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>
+						<el-form-item prop="domain" style="display: none"></el-form-item>
 					</div>
 				</div>
 			</div>
-			<el-form ref="ruleFormRef" :model="formData" :rules="dataRules" label-width="90px"
-				class="flex flex-wrap mt-4">
+			<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"
-						 :styleClass="'w-full'" />
+					<JDictSelect v-model:value="formData.action" placeholder="请选择推送方式" dictType="pushMode" :styleClass="'w-full'" />
 				</el-form-item>
 
 				<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="推送图片" prop="pushContent" class="w-1/2 ">
+				<el-form-item label="推送延时" prop="delayPush" class="w-1/2">
+					<div class="flex items-start w-full">
+						<el-input v-model="formData.delayPush" type="text" placeholder="请输入推送延时" />
+					</div>
+				</el-form-item>
+				<el-form-item label="推送图片" prop="pushContent" class="w-full">
 					<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" />
+						<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>
 
 				<el-form-item v-if="!formData.pushType" label="推送内容" prop="pushContent" class="w-full">
-					<el-input v-model="formData.pushContent" type="textarea" placeholder="请输入推送内容" />
+					<el-input v-model="formData.pushContent" type="text" placeholder="请输入推送内容" />
 				</el-form-item>
 				<el-form-item label="备注" prop="remark" class="w-full">
 					<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
@@ -87,8 +110,7 @@
 		<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>
@@ -110,7 +132,7 @@ const props = defineProps({
 	},
 	rowData: {
 		type: Object,
-		default: () => { },
+		default: () => {},
 	},
 });
 const onCancel = () => {
@@ -131,7 +153,6 @@ const loading = ref(false);
 
 const ruleFormRef = ref();
 
-
 const formData = ref<any>({
 	ruleName: '',
 	keyword: [],
@@ -142,11 +163,13 @@ const formData = ref<any>({
 	action: '1',
 	remark: '',
 	pushType: false,
+	delayPush:''
 });
 
 // // 表单校验规则
 const dataRules = reactive({
 	ruleName: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
+	delayPush: [{ required: true, message: '推送延时不能为空', trigger: 'blur' }],
 	keyword: [
 		{
 			required: true,
@@ -429,9 +452,9 @@ const handleInputConfirm = (type: string) => {
 };
 const getpushContent = () => {
 	if (formData.value.pushContent && formData.value.pushType) {
-		return formData.value.pushContent.includes('http') ? formData.value.pushContent : baseURL + formData.value.pushContent
+		return formData.value.pushContent.includes('http') ? formData.value.pushContent : baseURL + formData.value.pushContent;
 	} else {
-		return formData.value.pushContent
+		return formData.value.pushContent;
 	}
 };
 
@@ -441,6 +464,14 @@ const onSubmit = async () => {
 		await ruleFormRef.value.validate();
 	} catch (e) {
 		return;
+	}
+	 let pushFrequency = formData.value.pushFrequency;
+	if (typeof pushFrequency === 'string' && pushFrequency.includes('%')) {
+		// 如果包含百分比符号,去除百分比符号并除以100
+		const num = parseFloat(pushFrequency.replace('%', ''));
+		if (!isNaN(num)) {
+			pushFrequency = num / 100;
+		}
 	}
 	formData.value = {
 		...formData.value,
@@ -449,6 +480,7 @@ const onSubmit = async () => {
 		domain: domainData.value,
 		pushContent: getpushContent(),
 		pushType: formData.value.pushType || false,
+		pushFrequency: pushFrequency.toString(),
 	};
 	if (!formData.value.pushContent) {
 		return useMessage().error(formData.value.pushType ? '推送图片不能为空' : '推送内容不能为空');
@@ -469,6 +501,16 @@ const onSubmit = async () => {
 		loading.value = false;
 	}
 };
+// 格式化数据展示
+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 '';
+};
 
 watch(
 	() => props.open,
@@ -477,6 +519,8 @@ watch(
 			formData.value = {
 				...props.rowData,
 				action: props.rowData?.action || '1',
+				pushFrequency: formatNum(props.rowData?.pushFrequency) || '',
+
 			};
 			ipData.value = props.rowData.ip || [];
 			keywordData.value = props.rowData.keyword || [];
@@ -484,8 +528,6 @@ watch(
 			oldUrl.value = props.rowData.pushContent;
 			console.log(formData.value);
 			console.log(props.rowData);
-			
-			
 		}
 	}
 );

+ 30 - 7
src/views/marketing/rules/index.vue

@@ -15,8 +15,7 @@
 					<el-form-item>
 						<el-button @click="getDataList" type="primary">查询</el-button>
 						<el-button @click="resetQuery" icon="Refresh">重置</el-button>
-						<el-button @click="openEdit({})"  type="primary"> 新建规则 </el-button>
-
+						<el-button @click="openEdit({})" type="primary"> 新建规则 </el-button>
 					</el-form-item>
 				</el-form>
 			</el-row>
@@ -33,17 +32,31 @@
 				<el-table-column label="关键字" prop="keyword" show-overflow-tooltip></el-table-column>
 				<el-table-column label="IP" prop="ip" show-overflow-tooltip></el-table-column>
 				<el-table-column label="域名" prop="domain" show-overflow-tooltip width="200"></el-table-column>
-				<el-table-column label="推送内容" prop="pushContent" show-overflow-tooltip width="200"></el-table-column>
-				<el-table-column label="推送频率" prop="pushFrequency" show-overflow-tooltip width="100"></el-table-column>
+				<el-table-column label="推送内容" prop="pushContent" show-overflow-tooltip width="200">
+					<template #default="{ row }">
+						<el-image v-if="row.pushType" :src="row.pushContent" style="width: 100px; height: 100px" />
+						<div v-else>{{ row.pushContent }}</div>
+					</template>
+				</el-table-column>
+				<el-table-column label="推送频率" prop="pushFrequency" show-overflow-tooltip width="100">
+					<template #default="{ row }">
+						{{ formatNum(row.pushFrequency) }}
+					</template>
+				</el-table-column>
+				<el-table-column label="延时推送" prop="delayPush" show-overflow-tooltip width="100">
+					<template #default="{ row }">
+						{{ row.delayPush }}s
+					</template>
+				</el-table-column>
 				<el-table-column label="备注" prop="remark" show-overflow-tooltip width="200"></el-table-column>
-				<el-table-column label="操作" width="100">
+				<el-table-column label="操作" width="100" fixed="right">
 					<template #default="{ row }">
 						<el-button @click="openEdit(row)" size="small" text type="primary"> 编辑 </el-button>
 						<el-button @click="(delOpen = true), (delObj = row)" size="small" text type="primary"> 删除 </el-button>
 					</template>
 				</el-table-column>
 			</el-table>
-			<Edit v-model:open="open" :row-data="rowData" @close="open = false"  @onsuccess="getDataList"/>
+			<Edit v-model:open="open" :row-data="rowData" @close="open = false" @onsuccess="getDataList" />
 			<el-dialog v-model="delOpen" title="提示" width="500" @close="delOpen = false">
 				<span>确认删除吗?</span>
 				<template #footer>
@@ -72,6 +85,16 @@ const rowData = ref({});
 const delOpen = ref(false);
 const loading = ref(false);
 const delObj = ref({});
+// 格式化数据展示
+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 '--';
+};
 
 // 定义变量内容
 const queryRef = ref();
@@ -101,7 +124,7 @@ const exportExcel = () => {
 };
 const openEdit = (row: any) => {
 	console.log(row);
-	
+
 	rowData.value = row;
 	open.value = true;
 };