feat(comments): 添加邮箱黑名单功能

实现评论系统的邮箱黑名单功能,包括:
1. 在API中添加邮箱黑名单检查逻辑
2. 在管理后台添加邮箱黑名单设置界面
3. 支持在评论列表中直接屏蔽邮箱
4. 添加相关API接口处理邮箱黑名单操作
This commit is contained in:
anghunk
2026-01-21 10:03:00 +08:00
parent 49962db38d
commit aa5172eb84
5 changed files with 161 additions and 30 deletions

View File

@@ -44,6 +44,7 @@ export type CommentSettingsResponse = {
adminKeySet?: boolean;
requireReview?: boolean;
blockedIps?: string[];
blockedEmails?: string[];
};
export type EmailNotifySettingsResponse = {
@@ -139,6 +140,7 @@ export function saveCommentSettings(data: {
adminKey?: string;
requireReview?: boolean;
blockedIps?: string[];
blockedEmails?: string[];
}): Promise<{ message: string }> {
return put<{ message: string }>('/admin/settings/comments', data);
}
@@ -147,6 +149,10 @@ export function blockIp(ip: string): Promise<{ message: string }> {
return post<{ message: string }>('/admin/comments/block-ip', { ip });
}
export function blockEmail(email: string): Promise<{ message: string }> {
return post<{ message: string }>('/admin/comments/block-email', { email });
}
export function exportComments(): Promise<any[]> {
return get<any[]>('/admin/comments/export');
}

View File

@@ -36,7 +36,15 @@
/>
<div class="cell-author-main">
<div class="cell-author-name">{{ item.name }}</div>
<div class="cell-author-email">{{ item.email }}</div>
<div class="cell-author-email">
<span
class="cell-email-text"
@click="handleBlockEmail(item)"
title="屏蔽该邮箱"
>
{{ item.email }}
</span>
</div>
<span class="cell-time">{{ formatDate(item.created) }}</span>
<div v-if="item.ipAddress" class="cell-author-ip">
<span class="cell-ip-text" @click="handleBlockIp(item)" title="屏蔽该 IP">{{ item.ipAddress }}</span>
@@ -180,6 +188,7 @@ import {
deleteComment,
updateCommentStatus,
blockIp,
blockEmail,
} from "../api/admin";
const comments = ref<CommentItem[]>([]);
@@ -311,6 +320,21 @@ async function handleBlockIp(item: CommentItem) {
}
}
async function handleBlockEmail(item: CommentItem) {
if (!item.email) {
return;
}
if (!window.confirm(`确认将邮箱 ${item.email} 加入黑名单吗?`)) {
return;
}
try {
const res = await blockEmail(item.email);
window.alert(res.message || "已加入邮箱黑名单");
} catch (e: any) {
error.value = e.message || "屏蔽邮箱失败";
}
}
onMounted(() => {
loadComments();
});
@@ -479,6 +503,14 @@ onMounted(() => {
margin-bottom: 2px;
}
.cell-email-text {
cursor: pointer;
}
.cell-email-text:hover {
text-decoration: underline;
}
.cell-content-text {
font-size: 13px;
line-height: 1.5;

View File

@@ -39,27 +39,9 @@
<input v-model="avatarPrefix" class="form-input" type="text" />
</div>
<h3 class="card-title">安全设置</h3>
<div class="form-item">
<label class="form-label">允许调用的域名多个域名用逗号分隔留空则不限制设置后仅匹配域名可调用前台评论组件</label>
<textarea
v-model="allowedDomains"
class="form-input"
rows="3"
placeholder="例如: example.com, test.com"
></textarea>
</div>
<div class="form-item">
<label class="form-label">IP 黑名单多个 IP 用逗号或换行分隔留空则不限制</label>
<textarea
v-model="blockedIps"
class="form-input"
rows="3"
placeholder="例如: 1.1.1.1, 2.2.2.2"
></textarea>
</div>
<div class="form-item">
<label class="form-label">管理员评论密钥</label>
<div class="form-hint" style="margin-bottom: 4px;">
<div class="form-hint" style="margin-bottom: 4px">
设置后前台使用管理员邮箱评论需输入此密钥
</div>
<input
@@ -69,6 +51,40 @@
autocomplete="new-password"
/>
</div>
<div class="form-item">
<label class="form-label"
>允许调用的域名多个域名用逗号分隔留空则不限制设置后仅匹配域名可调用前台评论组件</label
>
<textarea
v-model="allowedDomains"
class="form-input"
rows="3"
placeholder="例如: example.com, test.com"
></textarea>
</div>
<div class="form-item">
<label class="form-label"
>IP 黑名单多个 IP 用逗号或换行分隔留空则不限制</label
>
<textarea
v-model="blockedIps"
class="form-input"
rows="3"
placeholder="例如: 1.1.1.1, 2.2.2.2"
></textarea>
</div>
<div class="form-item">
<label class="form-label"
>邮箱黑名单多个邮箱用逗号或换行分隔留空则不限制</label
>
<textarea
v-model="blockedEmails"
class="form-input"
rows="3"
placeholder="例如: spam@example.com, bot@test.com"
></textarea>
</div>
<div class="card-actions">
<button class="card-button" :disabled="savingComment" @click="saveComment">
<span v-if="savingComment">保存中...</span>
@@ -205,7 +221,7 @@
</button>
<button
class="card-button secondary"
style="margin-left: auto;"
style="margin-left: auto"
@click="resetTemplatesToDefault"
>
恢复默认模板
@@ -315,6 +331,7 @@ const avatarPrefix = ref("");
const commentAdminEnabled = ref(false);
const allowedDomains = ref("");
const blockedIps = ref("");
const blockedEmails = ref("");
const commentAdminKey = ref("");
const adminKeySet = ref(false);
const requireReview = ref(false);
@@ -375,8 +392,9 @@ async function load() {
allowedDomains.value = commentRes.allowedDomains
? commentRes.allowedDomains.join(", ")
: "";
blockedIps.value = commentRes.blockedIps
? commentRes.blockedIps.join(", ")
blockedIps.value = commentRes.blockedIps ? commentRes.blockedIps.join(", ") : "";
blockedEmails.value = commentRes.blockedEmails
? commentRes.blockedEmails.join(", ")
: "";
commentAdminKey.value = commentRes.adminKey || "";
adminKeySet.value = !!commentRes.adminKeySet;
@@ -384,10 +402,8 @@ async function load() {
emailGlobalEnabled.value = !!emailNotifyRes.globalEnabled;
if (emailNotifyRes.templates) {
templateAdmin.value =
emailNotifyRes.templates.admin || DEFAULT_ADMIN_TEMPLATE;
templateReply.value =
emailNotifyRes.templates.reply || DEFAULT_REPLY_TEMPLATE;
templateAdmin.value = emailNotifyRes.templates.admin || DEFAULT_ADMIN_TEMPLATE;
templateReply.value = emailNotifyRes.templates.reply || DEFAULT_REPLY_TEMPLATE;
} else {
templateAdmin.value = DEFAULT_ADMIN_TEMPLATE;
templateReply.value = DEFAULT_REPLY_TEMPLATE;
@@ -515,6 +531,10 @@ async function saveComment() {
.split(/[,\n]/)
.map((d) => d.trim())
.filter(Boolean),
blockedEmails: blockedEmails.value
.split(/[,\n]/)
.map((d) => d.trim())
.filter(Boolean),
});
showToast(res.message || "保存成功", "success");