实现敏感词检测后,失败发生邮箱

This commit is contained in:
2025-11-22 14:03:40 +08:00
parent c2cac51762
commit f3a9926caf
35 changed files with 1233 additions and 43916 deletions

View File

@@ -587,7 +587,7 @@ async function loadSelectOptions(reset = false) {
// 加载资源列表
result = await resourceApi.getResourcePage(
selectPageParam.value,
searchKeyword.value ? { keyword: searchKeyword.value } : undefined
searchKeyword.value ? { title: searchKeyword.value } : undefined
);
} else if (currentBanner.value.linkType === 2) {
// 加载课程列表

View File

@@ -0,0 +1,377 @@
<template>
<div class="sensitive-management">
<div class="header">
<h2>敏感词管理</h2>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
添加敏感词
</el-button>
</div>
<!-- 搜索过滤区域 -->
<div class="filter-section">
<el-form :model="filterForm" inline>
<el-form-item label="敏感词">
<el-input
v-model="filterForm.word"
placeholder="请输入敏感词"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="类型">
<el-select v-model="filterForm.type" class="sensitive-type-selector" placeholder="请选择类型" clearable>
<el-option label="禁用词" value="deny" />
<el-option label="允许词" value="allow" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 数据表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="sensitiveList"
style="width: 100%"
border
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="word" label="敏感词" min-width="200" />
<el-table-column prop="type" label="类型" width="120">
<template #default="{ row }">
<el-tag :type="row.type === 'deny' ? 'danger' : 'success'">
{{ row.type === 'deny' ? '禁用词' : '允许词' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleChangeType(row)"
>
{{ row.type === 'deny' ? '改为允许' : '改为禁用' }}
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
class="pagination-container"
v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pageParam.totalElements || 0"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 添加敏感词对话框 -->
<el-dialog
v-model="addDialogVisible"
title="添加敏感词"
width="500px"
@close="resetAddForm"
>
<el-form
ref="addFormRef"
:model="addForm"
:rules="addFormRules"
label-width="80px"
>
<el-form-item label="敏感词" prop="word">
<el-input
v-model="addForm.word"
placeholder="请输入敏感词"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-radio-group v-model="addForm.type">
<el-radio label="deny">禁用词</el-radio>
<el-radio label="allow">允许词</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAdd" :loading="addLoading">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive } from 'vue';
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import { sensitiveApi } from '@/apis/resource/sensitive';
import type { SensitiveWord, PageParam } from '@/types';
const loading = ref(false);
const addLoading = ref(false);
const addDialogVisible = ref(false);
const addFormRef = ref<FormInstance>();
const pageParam = ref<PageParam>({
pageNumber: 1,
pageSize: 10,
totalElements: 0,
});
const sensitiveList = ref<SensitiveWord[]>([]);
// 过滤表单
const filterForm = reactive<Partial<SensitiveWord>>({
word: '',
type: '',
});
// 添加表单
const addForm = reactive<Partial<SensitiveWord>>({
word: '',
type: 'deny',
});
// 添加表单验证规则
const addFormRules: FormRules = {
word: [
{ required: true, message: '请输入敏感词', trigger: 'blur' },
{ min: 1, max: 50, message: '敏感词长度在 1 到 50 个字符', trigger: 'blur' },
],
type: [
{ required: true, message: '请选择类型', trigger: 'change' },
],
};
// 获取敏感词分页数据
async function getSensitivePage() {
try {
loading.value = true;
const filter: Partial<SensitiveWord> = {};
if (filterForm.word) filter.word = filterForm.word;
if (filterForm.type) filter.type = filterForm.type;
const res = await sensitiveApi.getSensitivePage(pageParam.value, filter as SensitiveWord);
if (res.success) {
pageParam.value.totalElements = res.pageDomain?.pageParam?.totalElements || 0;
sensitiveList.value = res.pageDomain?.dataList || [];
} else {
ElMessage.error(res.message || '获取敏感词列表失败');
}
} catch (error) {
console.error('获取敏感词列表失败:', error);
ElMessage.error('获取敏感词列表失败');
} finally {
loading.value = false;
}
}
// 搜索
function handleSearch() {
pageParam.value.pageNumber = 1;
getSensitivePage();
}
// 重置搜索
function handleReset() {
Object.assign(filterForm, {
word: '',
type: '',
});
pageParam.value.pageNumber = 1;
getSensitivePage();
}
// 分页大小改变
function handleSizeChange(size: number) {
pageParam.value.pageSize = size;
pageParam.value.pageNumber = 1;
getSensitivePage();
}
// 当前页改变
function handleCurrentChange(page: number) {
pageParam.value.pageNumber = page;
getSensitivePage();
}
// 显示添加对话框
function showAddDialog() {
addDialogVisible.value = true;
}
// 重置添加表单
function resetAddForm() {
addFormRef.value?.resetFields();
Object.assign(addForm, {
word: '',
type: 'deny',
});
}
// 添加敏感词
async function handleAdd() {
if (!addFormRef.value) return;
try {
const valid = await addFormRef.value.validate();
if (!valid) return;
addLoading.value = true;
const res = await sensitiveApi.addSensitiveWord(addForm as SensitiveWord);
if (res.success) {
ElMessage.success('添加敏感词成功');
addDialogVisible.value = false;
getSensitivePage();
} else {
ElMessage.error(res.message || '添加敏感词失败');
}
} catch (error) {
console.error('添加敏感词失败:', error);
ElMessage.error('添加敏感词失败');
} finally {
addLoading.value = false;
}
}
// 修改敏感词类型
async function handleChangeType(row: SensitiveWord) {
try {
const newType = row.type === 'deny' ? 'allow' : 'deny';
const typeText = newType === 'deny' ? '禁用词' : '允许词';
await ElMessageBox.confirm(
`确定要将敏感词 "${row.word}" 改为${typeText}吗?`,
'确认修改',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
const updateData = { ...row, type: newType };
const res = await sensitiveApi.changeSensitiveWordType(updateData);
if (res.success) {
ElMessage.success('修改敏感词类型成功');
getSensitivePage();
} else {
ElMessage.error(res.message || '修改敏感词类型失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('修改敏感词类型失败:', error);
ElMessage.error('修改敏感词类型失败');
}
}
}
// 删除敏感词
async function handleDelete(row: SensitiveWord) {
try {
await ElMessageBox.confirm(
`确定要删除敏感词 "${row.word}" 吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
const res = await sensitiveApi.deleteSensitiveWord(row);
if (res.success) {
ElMessage.success('删除敏感词成功');
getSensitivePage();
} else {
ElMessage.error(res.message || '删除敏感词失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除敏感词失败:', error);
ElMessage.error('删除敏感词失败');
}
}
}
onMounted(() => {
getSensitivePage();
});
</script>
<style lang="scss" scoped>
.sensitive-management {
padding: 20px;
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
color: #303133;
}
}
.filter-section {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.sensitive-type-selector {
width: 100px;
}
}
.table-section {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
}
</style>

View File

@@ -81,7 +81,7 @@ import { useRouter } from 'vue-router';
import { resourceApi, resourceTagApi } from '@/apis/resource'
import type { PageParam, ResourceSearchParams, Resource, Tag } from '@/types';
import { ArticleShowView } from '@/views/public/article';
import { ArticleStatus } from '@/types/enums';
import { ResourceStatus } from '@/types/enums';
const router = useRouter();
const searchKeyword = ref('');
@@ -166,8 +166,8 @@ function editArticle(row: any) {
async function changeArticleStatus(row: Resource) {
try {
// status: 0-草稿, 1-已发布, 2-已下架
if (row.status === ArticleStatus.DRAFT || row.status === ArticleStatus.OFFLINE) {
// status: 0-草稿, 1-已发布, 2-已下架, 3-审核中, 4-敏感词未通过
if (row.status === ResourceStatus.DRAFT || row.status === ResourceStatus.OFFLINE || row.status === ResourceStatus.SENSITIVE_FAILED) {
// 草稿或下架状态 -> 发布
const res = await resourceApi.publishResource(row.resourceID!);
if (res.success) {
@@ -176,7 +176,7 @@ async function changeArticleStatus(row: Resource) {
} else {
ElMessage.error('发布失败');
}
} else if (row.status === ArticleStatus.PUBLISHED) {
} else if (row.status === ResourceStatus.PUBLISHED) {
// 已发布状态 -> 下架
const res = await resourceApi.unpublishResource(row.resourceID!);
if (res.success) {
@@ -206,40 +206,44 @@ function deleteArticle() {
function getStatusType(status: number) {
const typeMap: Record<number, any> = {
[ArticleStatus.DRAFT]: 'info',
[ArticleStatus.PUBLISHED]: 'success',
[ArticleStatus.OFFLINE]: 'warning',
[ArticleStatus.FAILED]: 'danger'
[ResourceStatus.DRAFT]: 'info',
[ResourceStatus.PUBLISHED]: 'success',
[ResourceStatus.OFFLINE]: 'warning',
[ResourceStatus.REVIEWING]: 'primary',
[ResourceStatus.SENSITIVE_FAILED]: 'danger'
};
return typeMap[status] || 'info';
}
function getStatusText(status: number) {
const textMap: Record<number, string> = {
[ArticleStatus.DRAFT]: '草稿',
[ArticleStatus.PUBLISHED]: '已发布',
[ArticleStatus.OFFLINE]: '已下架',
[ArticleStatus.FAILED]: '审核失败'
[ResourceStatus.DRAFT]: '草稿',
[ResourceStatus.PUBLISHED]: '已发布',
[ResourceStatus.OFFLINE]: '已下架',
[ResourceStatus.REVIEWING]: '审核中',
[ResourceStatus.SENSITIVE_FAILED]: '敏感词未通过'
};
return textMap[status] || '未知';
}
function getActionButtonType(status: number) {
// 草稿下架状态显示主要按钮(发布), 已发布状态显示警告按钮(下架)
if (status === ArticleStatus.DRAFT || status === ArticleStatus.OFFLINE || status === ArticleStatus.FAILED) {
// 草稿下架或敏感词未通过状态显示主要按钮(发布), 已发布状态显示警告按钮(下架)
if (status === ResourceStatus.DRAFT || status === ResourceStatus.OFFLINE || status === ResourceStatus.SENSITIVE_FAILED) {
return 'primary';
} else if (status === ArticleStatus.PUBLISHED) {
} else if (status === ResourceStatus.PUBLISHED) {
return 'warning';
}
return '';
}
function getActionButtonText(status: number) {
// 草稿下架状态显示"发布", 已发布状态显示"下架"
if (status === ArticleStatus.DRAFT || status === ArticleStatus.OFFLINE || status === ArticleStatus.FAILED) {
// 草稿下架或敏感词未通过状态显示"发布", 已发布状态显示"下架", 审核中状态不可操作
if (status === ResourceStatus.DRAFT || status === ResourceStatus.OFFLINE || status === ResourceStatus.SENSITIVE_FAILED) {
return '发布';
} else if (status === ArticleStatus.PUBLISHED) {
} else if (status === ResourceStatus.PUBLISHED) {
return '下架';
} else if (status === ResourceStatus.REVIEWING) {
return '审核中';
}
return '操作';
}

View File

@@ -223,7 +223,7 @@ async function handlePublish() {
await formRef.value?.validate();
publishing.value = true;
// 新建或立即发布时,明确标记为已发布
// 新建或"立即发布"时,明确标记为已发布
// 对新建文章status 没有值,这里设为 1
// 对草稿->发布:也会变成 1
articleForm.value.resource.status = 1;
@@ -233,18 +233,26 @@ async function handlePublish() {
if (props.collectionItemId) {
await handleConvertFromCollection();
} else {
const result = await resourceApi.createResource(articleForm.value);
let result;
if (isEdit.value) {
// 编辑模式:调用更新接口
result = await resourceApi.updateResource(articleForm.value);
} else {
// 新建模式:调用创建接口
result = await resourceApi.createResource(articleForm.value);
}
if (result.success) {
const resourceID = result.data?.resource?.resourceID || '';
ElMessage.success('发布成功');
const resourceID = result.data?.resource?.resourceID || articleForm.value.resource.resourceID || '';
ElMessage.success(isEdit.value ? '更新成功' : '发布成功');
emit('publish-success', resourceID);
} else {
ElMessage.error(result.message || '发布失败');
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '发布失败'));
}
}
} catch (error) {
console.error('发布失败:', error);
ElMessage.error('发布失败');
console.error(isEdit.value ? '更新失败:' : '发布失败:', error);
ElMessage.error(isEdit.value ? '更新失败' : '发布失败');
} finally {
publishing.value = false;
}
@@ -287,13 +295,29 @@ async function handleSaveDraft() {
savingDraft.value = true;
try {
// TODO: 调用API保存草稿
console.log('保存草稿:', articleForm);
// 设置为草稿状态
articleForm.value.resource.status = 0;
let result;
if (isEdit.value) {
// 编辑模式:调用更新接口
result = await resourceApi.updateResource(articleForm.value);
} else {
// 新建模式:调用创建接口
result = await resourceApi.createResource(articleForm.value);
}
await new Promise(resolve => setTimeout(resolve, 1000));
ElMessage.success('草稿已保存');
emit('save-draft-success');
if (result.success) {
// 如果是新建模式,需要更新为编辑模式
if (!isEdit.value && result.data?.resource?.resourceID) {
isEdit.value = true;
articleForm.value.resource.resourceID = result.data.resource.resourceID;
}
ElMessage.success('草稿已保存');
emit('save-draft-success');
} else {
ElMessage.error(result.message || '保存失败');
}
} catch (error) {
console.error('保存失败:', error);
ElMessage.error('保存失败');

View File

@@ -17,9 +17,10 @@
<select v-model="searchForm.status" class="form-select">
<option :value="undefined">请选择状态</option>
<option :value="0">未上线</option>
<option :value="1">上线</option>
<option :value="2">下架</option>
<option :value="3">审核失败</option>
<option :value="1">发布</option>
<option :value="2">下架</option>
<option :value="3">审核</option>
<option :value="4">敏感词未通过</option>
</select>
</div>
<div class="form-item">
@@ -62,12 +63,13 @@
<el-table-column prop="duration" label="时长(分钟)" width="120" />
<el-table-column prop="learnCount" label="学习人数" width="100" />
<el-table-column prop="viewCount" label="浏览次数" width="100" />
<el-table-column label="状态" width="100">
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tag v-if="row.status === 0" type="info">未上线</el-tag>
<el-tag v-else-if="row.status === 1" type="success">上线</el-tag>
<el-tag v-else-if="row.status === 2" type="warning">下架</el-tag>
<el-tag v-else-if="row.status === 3" type="danger">审核失败</el-tag>
<el-tag v-else-if="row.status === 1" type="success">发布</el-tag>
<el-tag v-else-if="row.status === 2" type="warning">下架</el-tag>
<el-tag v-else-if="row.status === 3" type="primary">审核</el-tag>
<el-tag v-else-if="row.status === 4" type="danger">敏感词未通过</el-tag>
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="80" />
@@ -77,13 +79,13 @@
编辑
</el-button>
<el-button
v-if="row.status === 0 || row.status === 2 || row.status === 3"
v-if="row.status === 0 || row.status === 2 || row.status === 4"
type="success"
size="small"
link
@click="handleUpdateStatus(row, 1)"
>
上线
发布
</el-button>
<el-button
v-if="row.status === 1"