Pārlūkot izejas kodu

营销规则静态页面编写

jcq 5 dienas atpakaļ
vecāks
revīzija
2d9d43663b

+ 3 - 3
src/views/count/user/activeUser/components/AddTrend.vue

@@ -19,7 +19,7 @@
 					</el-select>
 
 					<!-- 版本对比和渠道对比使用popover -->
-					<el-popover v-if="industryCompare !== 'time' && !industryCompare" placement="bottom" trigger="click" width="400">
+					<el-popover v-if="industryCompare !== 'time' && industryCompare" placement="bottom" trigger="click" width="400">
 						<template #reference>
 							<el-button class="ml-2">{{ t('addUser.average') }}</el-button>
 						</template>
@@ -251,9 +251,11 @@ const getData = async (type: string) => {
 		res = await getViscosity(formData.value);
 		data = res.data || [];
 	} else if (chartType.value == '周活跃度') {
+		formData.value.timeUnit = 'week';
 		res = await getWeekrate(formData.value);
 		data = res.data || [];
 	} else if (chartType.value == '月活跃度') {
+		formData.value.timeUnit = 'month';
 		res = await getMonthrate(formData.value);
 		data = res.data || [];
 	}
@@ -286,8 +288,6 @@ const initChartData = async (data: any) => {
 	} else {
 		lineChartData.value.items.push(
 			data.items.map((item: any, index: number) => {
-				console.log(item);
-
 				const randomColorIndex = Math.floor(Math.random() * colorSchemes.length);
 				return {
 					name: getName(item, index, data),

+ 1 - 1
src/views/count/user/adduser/components/AddTrend.vue

@@ -36,7 +36,7 @@
 					</el-select>
 
 					<!-- 版本对比和渠道对比使用popover -->
-					<el-popover v-if="industryCompare !== 'time' && !industryCompare" placement="bottom" trigger="click"
+					<el-popover v-if="industryCompare !== 'time' && industryCompare" placement="bottom" trigger="click"
 						width="400">
 						<template #reference>
 							<el-button class="ml-2">{{ t('addUser.average') }}</el-button>

+ 1 - 1
src/views/marketing/config/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<div class="layout-padding">
 		<el-tabs v-model="activeName" type="card" class="demo-tabs" @tab-click="handleClick">
-			<el-tab-pane :label="t('marketingConfig.ipList')" name="IP分组" class="layout-padding-auto layout-padding-view">
+			<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>

+ 448 - 0
src/views/marketing/rules/components/Edit.vue

@@ -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>

+ 103 - 0
src/views/marketing/rules/index.vue

@@ -0,0 +1,103 @@
+<template>
+	<div class="layout-padding">
+		<div class="layout-padding-auto layout-padding-view">
+			<el-row class="ml10" v-show="showSearch">
+				<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
+					<el-form-item label="关键字" prop="logType">
+						<el-input placeholder="请输入关键字"  v-model="state.queryForm.queryIPName" />
+					</el-form-item>
+          <el-form-item label="IP" prop="logType">
+						<el-input placeholder="请输入IP"  v-model="state.queryForm.queryIPName" />
+					</el-form-item>
+					<el-form-item label="域名" prop="logType">
+						<el-input placeholder="请输入域名"  v-model="state.queryForm.queryIPName" />
+					</el-form-item>
+					<el-form-item>
+						<el-button @click="getDataList" type="primary">查询</el-button>
+						<el-button @click="resetQuery" icon="Refresh">重置</el-button>
+					</el-form-item>
+				</el-form>
+			</el-row>
+		
+			<el-table
+				:data="state.dataList"
+				v-loading="state.loading"
+				border
+				:cell-style="tableStyle.cellStyle"
+				:header-cell-style="tableStyle.headerCellStyle"
+			>
+				<el-table-column label="规则名称" prop="title" show-overflow-tooltip></el-table-column>
+				<el-table-column label="关键字" prop="remoteAddr" show-overflow-tooltip></el-table-column>
+				<el-table-column label="IP" prop="method" show-overflow-tooltip></el-table-column>
+				<el-table-column label="域名" prop="createTime" show-overflow-tooltip width="200"></el-table-column>
+				<el-table-column label="触发规则" prop="createBy" show-overflow-tooltip width="200"></el-table-column>
+				<el-table-column label="备注" prop="createBy" show-overflow-tooltip width="200"></el-table-column>
+				<el-table-column label="操作" width="150">
+					<template #default="scope">
+						<el-button @click="openEdit(scope.row)" size="small" text type="primary">
+							编辑
+						</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+      <Edit v-model:open="open" @close="open = false" />
+			<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination"> </pagination>
+		</div>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { delObj, pageList } from '/@/api/admin/log';
+import { useI18n } from 'vue-i18n';
+
+
+const Edit = defineAsyncComponent(() => import('./components/Edit.vue'));
+
+const open = ref(false);
+const rowData = ref({});
+const { t } = useI18n();
+
+// 定义变量内容
+const queryRef = ref();
+const showSearch = ref(true);
+
+
+const state: BasicTableProps = reactive<BasicTableProps>({
+	queryForm: {
+		logType: '',
+		createTime: '',
+	},
+	selectObjs: [],
+	pageList: pageList,
+	descs: ['create_time'],
+});
+
+//  table hook
+const { downBlobFile, getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
+
+// 清空搜索条件
+const resetQuery = () => {
+	queryRef.value?.resetFields();
+	getDataList();
+};
+
+// 导出excel
+const exportExcel = () => {
+	downBlobFile('/admin/log/export', state.queryForm, 'log.xlsx');
+};
+const openEdit = (row: any) => {
+  rowData.value = row;
+  open.value = true;
+
+}
+
+
+
+</script>
+
+<style lang="scss" scoped>
+pre code.hljs {
+	width: 65%;
+}
+</style>