feat(config): implement dynamic security blocklist and home marketing popup with admin controls
这个提交包含在:
@@ -239,6 +239,145 @@
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Tab 6: 自定义敏感词与私下引流过滤库 (P2) -->
|
||||
<a-tab-pane key="security_words_config" title="引流风控与敏感词">
|
||||
<div class="tab-pane-content">
|
||||
<a-alert type="warning" style="margin-bottom: 20px">
|
||||
在此维护平台自定义敏感词与私下交易拦截词库(如联系方式变体、私下转账、违规引流等)。管理员保存后系统后端实时热加载生效,小程序端发帖、评论及订单备注将实时阻断拦截。
|
||||
</a-alert>
|
||||
|
||||
<a-card title="自定义敏感词黑名单库" :bordered="false" style="background: var(--color-fill-2)">
|
||||
<template #extra>
|
||||
<a-button type="outline" size="small" @click="addSecurityWord">
|
||||
<template #icon><icon-plus /></template>
|
||||
添加违规词
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-space wrap size="medium">
|
||||
<a-tag
|
||||
v-for="(word, index) in securityWordsList"
|
||||
:key="index"
|
||||
color="red"
|
||||
closable
|
||||
size="large"
|
||||
@close="removeSecurityWord(index)"
|
||||
>
|
||||
{{ word }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="快捷批量录入敏感词(多个词请用换行或逗号隔开)">
|
||||
<a-textarea
|
||||
v-model="batchWordsInput"
|
||||
placeholder="如:\n私下微信交易\n线下现金\n加v优惠"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-button type="secondary" size="small" @click="handleBatchAddWords">
|
||||
解析并导入违规词库
|
||||
</a-button>
|
||||
</a-form>
|
||||
|
||||
<div style="margin-top: 24px">
|
||||
<a-button type="primary" :loading="saving" @click="saveSecurityWordsConfig">
|
||||
保存敏感词库(实时热生效)
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Tab 7: 首页运营全局弹窗 (P3) -->
|
||||
<a-tab-pane key="home_popup_config" title="首页运营全局弹窗">
|
||||
<div class="tab-pane-content">
|
||||
<a-alert type="info" style="margin-bottom: 20px">
|
||||
配置小程序首页首次或每日打开时弹出的营销/活动全局浮层弹窗(如新人首单特惠礼包、节日专属组局返现、官方安全温馨提示等)。
|
||||
</a-alert>
|
||||
|
||||
<a-card title="全局浮层营销弹窗配置" :bordered="false" style="background: var(--color-fill-2)">
|
||||
<a-form :model="homePopupForm.valueJson" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="弹窗开关">
|
||||
<a-switch v-model="homePopupForm.valueJson.enabled" checked-text="开启弹窗" unchecked-text="关闭隐藏" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="弹出频次规则">
|
||||
<a-select v-model="homePopupForm.valueJson.frequency">
|
||||
<a-option value="once_daily">每天首次打开弹出一次(推荐)</a-option>
|
||||
<a-option value="once_ever">每个用户仅弹出一次</a-option>
|
||||
<a-option value="always">每次冷启动打开均弹出</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="弹窗主标题">
|
||||
<a-input v-model="homePopupForm.valueJson.title" placeholder="如:新人专享同城陪伴好礼" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="弹窗副标题/引导语">
|
||||
<a-input v-model="homePopupForm.valueJson.subtitle" placeholder="如:注册即送20元无门槛立减券" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="弹窗海报图片 URL">
|
||||
<div style="display: flex; gap: 12px; align-items: flex-start; width: 100%;">
|
||||
<a-input v-model="homePopupForm.valueJson.imageUrl" placeholder="可直接输入图片公网URL或上传" style="flex: 1;" />
|
||||
<a-upload
|
||||
action="/api/upload"
|
||||
name="file"
|
||||
:show-file-list="false"
|
||||
@success="handlePopupUploadSuccess"
|
||||
@error="handlePopupUploadError"
|
||||
>
|
||||
<template #upload-button>
|
||||
<a-button type="outline">
|
||||
<template #icon><icon-upload /></template>
|
||||
上传海报
|
||||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
</div>
|
||||
<div v-if="homePopupForm.valueJson.imageUrl" style="margin-top: 10px;">
|
||||
<a-image :src="homePopupForm.valueJson.imageUrl" :height="120" fit="cover" style="border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="按钮操作文本">
|
||||
<a-input v-model="homePopupForm.valueJson.btnText" placeholder="如:立即领券组局" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="点击跳转路径 (小程序内部页面)">
|
||||
<a-input v-model="homePopupForm.valueJson.link" placeholder="如:/pages/activity/list 或 /pages/user/index" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div style="margin-top: 16px">
|
||||
<a-button type="primary" :loading="saving" @click="saveHomePopupConfig">
|
||||
保存首页弹窗配置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
@@ -336,6 +475,82 @@ const removeFaqItem = (index: number) => {
|
||||
faqList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// P2 敏感词库
|
||||
const securityWordsList = ref<string[]>([
|
||||
'加微信私聊', '私下走微信', '走线下免佣金', '线下给现金', '私下单',
|
||||
'联系方式v', 'vx号', '微信转账', '私加好友', '线下私约'
|
||||
]);
|
||||
const batchWordsInput = ref('');
|
||||
|
||||
const addSecurityWord = () => {
|
||||
const newWord = window.prompt('请输入需要拦截的违规敏感词');
|
||||
if (newWord && newWord.trim()) {
|
||||
const w = newWord.trim();
|
||||
if (!securityWordsList.value.includes(w)) {
|
||||
securityWordsList.value.push(w);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeSecurityWord = (index: number) => {
|
||||
securityWordsList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleBatchAddWords = () => {
|
||||
if (!batchWordsInput.value.trim()) {
|
||||
Message.warning('请输入需要批量导入的敏感词文本');
|
||||
return;
|
||||
}
|
||||
const splitWords = batchWordsInput.value
|
||||
.split(/[\n,,\s]+/)
|
||||
.map(w => w.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
let addedCount = 0;
|
||||
splitWords.forEach(w => {
|
||||
if (!securityWordsList.value.includes(w)) {
|
||||
securityWordsList.value.push(w);
|
||||
addedCount++;
|
||||
}
|
||||
});
|
||||
batchWordsInput.value = '';
|
||||
Message.success(`成功解析并新增了 ${addedCount} 个敏感词`);
|
||||
};
|
||||
|
||||
// P3 首页全局弹窗
|
||||
const homePopupForm = reactive({
|
||||
name: '小程序首页运营全局弹窗',
|
||||
description: '节日活动推广、新用户专享首单优惠全局浮层弹窗',
|
||||
status: 1,
|
||||
valueJson: {
|
||||
enabled: true,
|
||||
title: '新人专享同城陪伴好礼',
|
||||
subtitle: '注册即送20元无门槛立减券,首单专享特惠',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800',
|
||||
link: '/pages/activity/list',
|
||||
btnText: '立即领券组局',
|
||||
frequency: 'once_daily'
|
||||
}
|
||||
});
|
||||
|
||||
const handlePopupUploadSuccess = (fileItem: any) => {
|
||||
const response = fileItem.response;
|
||||
if (response && response.code === 20000 && response.data?.url) {
|
||||
let url = response.data.url;
|
||||
if (url.startsWith('/uploads/')) {
|
||||
url = `https://peiww.gynhc.com${url}`;
|
||||
}
|
||||
homePopupForm.valueJson.imageUrl = url;
|
||||
Message.success('弹窗海报上传成功并已持久化');
|
||||
} else {
|
||||
Message.warning('上传完成,但响应格式异常');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopupUploadError = () => {
|
||||
Message.error('图片上传失败,请检查文件大小及网络');
|
||||
};
|
||||
|
||||
const fetchConfigs = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -371,6 +586,15 @@ const fetchConfigs = async () => {
|
||||
topicsList.value = Array.isArray(item.valueJson) ? item.valueJson : [];
|
||||
} else if (item.configKey === 'faq_list') {
|
||||
faqList.value = Array.isArray(item.valueJson) ? item.valueJson : [];
|
||||
} else if (item.configKey === 'security_words') {
|
||||
if (Array.isArray(item.valueJson)) {
|
||||
securityWordsList.value = item.valueJson;
|
||||
}
|
||||
} else if (item.configKey === 'home_popup') {
|
||||
homePopupForm.name = item.name;
|
||||
homePopupForm.description = item.description;
|
||||
homePopupForm.status = item.status;
|
||||
homePopupForm.valueJson = { ...homePopupForm.valueJson, ...(item.valueJson || {}) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -436,6 +660,19 @@ const saveFaqConfig = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const saveSecurityWordsConfig = () => {
|
||||
saveConfig('security_words', {
|
||||
name: '私下引流与违规自定义敏感词库',
|
||||
description: '全平台发帖、评论及订单防私下交易关键词实时过滤',
|
||||
status: 1,
|
||||
valueJson: securityWordsList.value
|
||||
});
|
||||
};
|
||||
|
||||
const saveHomePopupConfig = () => {
|
||||
saveConfig('home_popup', homePopupForm);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchConfigs();
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户