Files
cwd/cwd-admin/src/views/SettingsView/index.vue
anghunk 49693ca46e feat(settings): 重构设置页面为标签页布局并提取通用样式
- 将设置页面从折叠面板改为标签页布局,提升操作体验
- 创建 common.less 文件集中管理全局样式(如 toast 动画)
- 移除各组件中重复的 toast 样式定义,统一维护
- 优化设置页面的视觉结构和交互逻辑
2026-02-02 17:09:46 +08:00

1088 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page">
<h2 class="page-title">网站设置</h2>
<div
v-if="toastVisible"
class="toast"
:class="toastType === 'error' ? 'toast-error' : 'toast-success'"
>
{{ toastMessage }}
</div>
<div v-if="loading" class="page-hint">加载中...</div>
<div v-else>
<div class="settings-tabs">
<button
type="button"
class="settings-tab"
:class="{ 'settings-tab-active': activeTab === 'comment' }"
@click="activeTab = 'comment'"
>
评论与安全
</button>
<button
type="button"
class="settings-tab"
:class="{ 'settings-tab-active': activeTab === 'feature' }"
@click="activeTab = 'feature'"
>
功能开关
</button>
<button
type="button"
class="settings-tab"
:class="{ 'settings-tab-active': activeTab === 'display' }"
@click="activeTab = 'display'"
>
后台显示
</button>
<button
type="button"
class="settings-tab"
:class="{ 'settings-tab-active': activeTab === 'notify' }"
@click="activeTab = 'notify'"
>
通知设置
</button>
</div>
<transition name="tab-fade" mode="out-in">
<div :key="activeTab">
<template v-if="activeTab === 'comment'">
<div class="card">
<div class="card-header">
<div class="card-title">评论显示配置</div>
</div>
<div class="card-body">
<div class="form-item">
<label class="form-label">管理员邮箱</label>
<input v-model="commentAdminEmail" class="form-input" type="email" />
</div>
<div class="form-item">
<label class="form-label">博主标签文字留空使用默认图标</label>
<input v-model="commentAdminBadge" class="form-input" type="text" />
</div>
<div class="form-item">
<label class="form-label">是否开启博主标签显示</label>
<label class="switch">
<input v-model="commentAdminEnabled" type="checkbox" />
<span class="slider" />
</label>
</div>
<div class="form-item">
<label class="form-label"
>头像前缀默认https://gravatar.com/avatar</label
>
<input v-model="avatarPrefix" class="form-input" type="text" />
</div>
<h3 class="card-title">安全设置</h3>
<div class="form-item">
<label class="form-label">新评论是否审核后再显示</label>
<label class="switch">
<input v-model="requireReview" type="checkbox" />
<span class="slider" />
</label>
</div>
<div class="form-item">
<label class="form-label">
管理员评论密钥设置后前台使用管理员邮箱评论需输入此密钥
</label>
<input
v-model="commentAdminKey"
class="form-input"
placeholder="输入密钥以设置或修改"
autocomplete="new-password"
/>
</div>
<div class="form-item">
<label class="form-label">
允许调用的域名设置后仅匹配域名可调用前台评论组件留空则不限制使用空格或者逗号进行分割
</label>
<div class="tag-input">
<div class="tag-input-inner">
<span
v-for="domain in allowedDomainTags"
:key="domain"
class="tag-input-tag"
>
<span class="tag-input-tag-text">{{ domain }}</span>
<button
type="button"
class="tag-input-tag-remove"
@click="removeAllowedDomain(domain)"
>
<PhTrash :size="14" />
</button>
</span>
<input
v-model="allowedDomainInput"
class="tag-input-input"
@keyup="handleAllowedDomainKeyup"
@blur="handleAllowedDomainBlur"
/>
</div>
</div>
</div>
<div class="form-item">
<label class="form-label">
IP 黑名单多个 IP 用逗号或换行分隔留空则不限制
</label>
<div class="tag-input">
<div class="tag-input-inner">
<span v-for="ip in blockedIpTags" :key="ip" class="tag-input-tag">
<span class="tag-input-tag-text">{{ ip }}</span>
<button
type="button"
class="tag-input-tag-remove"
@click="removeBlockedIp(ip)"
>
<PhTrash :size="14" />
</button>
</span>
<input
v-model="blockedIpInput"
class="tag-input-input"
@keyup="handleBlockedIpKeyup"
@blur="handleBlockedIpBlur"
/>
</div>
</div>
</div>
<div class="form-item">
<label class="form-label"
>邮箱黑名单多个邮箱用逗号或换行分隔留空则不限制</label
>
<div class="tag-input">
<div class="tag-input-inner">
<span
v-for="email in blockedEmailTags"
:key="email"
class="tag-input-tag"
>
<span class="tag-input-tag-text">{{ email }}</span>
<button
type="button"
class="tag-input-tag-remove"
@click="removeBlockedEmail(email)"
>
<PhTrash :size="14" />
</button>
</span>
<input
v-model="blockedEmailInput"
class="tag-input-input"
@keyup="handleBlockedEmailKeyup"
@blur="handleBlockedEmailBlur"
/>
</div>
</div>
</div>
<div class="card-actions">
<button
class="card-button"
:disabled="savingComment"
@click="saveComment"
>
<span v-if="savingComment">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</div>
</template>
<template v-else-if="activeTab === 'feature'">
<div class="card">
<div class="card-header">
<div class="card-title">功能开关</div>
</div>
<div class="card-body">
<div class="form-item">
<label class="form-label">开启文章点赞功能</label>
<label class="switch">
<input v-model="enableArticleLike" type="checkbox" />
<span class="slider" />
</label>
<div class="form-hint">
开启后评论区顶部会显示的文章点赞喜欢按钮
</div>
</div>
<div class="form-item">
<label class="form-label">开启评论点赞功能</label>
<label class="switch">
<input v-model="enableCommentLike" type="checkbox" />
<span class="slider" />
</label>
<div class="form-hint">
开启后评论列表中的每条评论都会显示点赞按钮
</div>
</div>
<div class="card-actions">
<button
class="card-button"
:disabled="savingFeature"
@click="saveFeature"
>
<span v-if="savingFeature">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</div>
</template>
<template v-else-if="activeTab === 'display'">
<div class="card">
<div class="card-header">
<div class="card-title">后台显示设置</div>
</div>
<div class="card-body">
<div class="form-item">
<label class="form-label">后台标题</label>
<input
v-model="adminLayoutTitle"
class="form-input"
type="text"
placeholder="CWD 评论系统"
/>
<div class="form-hint">显示在后台顶部导航栏左侧</div>
</div>
<div class="card-actions">
<button
class="card-button"
:disabled="savingDisplay"
@click="saveDisplay"
>
<span v-if="savingDisplay">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</div>
</template>
<template v-else-if="activeTab === 'notify'">
<div class="card">
<div class="card-header">
<div class="card-title">通知邮箱设置</div>
</div>
<div class="card-body">
<div class="form-item">
<label class="form-label">开启邮件通知</label>
<label class="switch">
<input v-model="emailGlobalEnabled" type="checkbox" />
<span class="slider" />
</label>
</div>
<div class="divider"></div>
<h4 class="card-subtitle">1. SMTP 发件配置</h4>
<div class="form-item">
<label class="form-label">邮件服务商</label>
<select
v-model="smtpProvider"
class="form-input"
@change="onProviderChange"
>
<option value="qq">QQ 邮箱</option>
<option value="custom">自定义 SMTP</option>
</select>
</div>
<div v-if="smtpProvider === 'custom'">
<div class="form-item">
<label class="form-label">SMTP 服务器</label>
<input
v-model="smtpHost"
class="form-input"
placeholder="smtp.example.com"
/>
</div>
<div class="form-item">
<label class="form-label">SMTP 端口</label>
<input
v-model="smtpPort"
class="form-input"
type="number"
placeholder="465"
/>
</div>
<div class="form-item">
<label class="form-label">SSL 安全连接</label>
<label class="switch">
<input v-model="smtpSecure" type="checkbox" />
<span class="slider" />
</label>
</div>
</div>
<div class="form-item">
<label class="form-label">发件邮箱账号</label>
<input
v-model="smtpUser"
class="form-input"
placeholder="例如: 123456@qq.com"
/>
</div>
<div class="form-item">
<label class="form-label">授权码/密码</label>
<input
v-model="smtpPass"
class="form-input"
type="text"
placeholder="QQ邮箱请使用授权码"
/>
<div v-if="smtpProvider === 'qq'" class="form-hint">
注意QQ 邮箱必须使用<a
href="https://service.mail.qq.com/detail/0/75"
target="_blank"
>授权码</a
>而非 QQ 密码<br />
请登录 QQ 邮箱网页版设置 - 账户中开启 POP3/SMTP
服务并生成授权码
</div>
</div>
<div class="divider"></div>
<h4 class="card-subtitle">2. 邮件模板设置</h4>
<div class="form-item">
<label class="form-label">管理员通知模板 (HTML)</label>
<div class="form-hint">
可用变量${commentAuthor} (评论人昵称), ${postTitle} (文章标题),
${postUrl} (文章链接), ${commentContent} (评论内容)
</div>
<textarea
v-model="templateAdmin"
class="form-input"
rows="10"
placeholder="留空则使用默认模板"
></textarea>
</div>
<div class="form-item">
<label class="form-label">回复通知模板 (HTML)</label>
<div class="form-hint">
可用变量${toName} (接收人昵称), ${replyAuthor} (回复人昵称),
${postTitle} (文章标题), ${postUrl} (文章链接), ${parentComment}
(原评论), ${replyContent} (回复内容)
</div>
<textarea
v-model="templateReply"
class="form-input"
rows="10"
placeholder="留空则使用默认模板"
></textarea>
</div>
<div
v-if="message && messageType === 'error'"
class="form-message form-message-error"
>
{{ message }}
</div>
<div class="card-actions" style="justify-content: space-between">
<button
class="card-button secondary"
:disabled="testingEmail"
@click="testEmail"
>
<span v-if="testingEmail">发送中...</span>
<span v-else>发送测试邮件</span>
</button>
<button
class="card-button secondary"
style="margin-left: auto"
@click="resetTemplatesToDefault"
>
恢复默认模板
</button>
<button class="card-button" :disabled="savingEmail" @click="saveEmail">
<span v-if="savingEmail">保存中...</span>
<span v-else>保存配置</span>
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">Telegram 通知设置</div>
</div>
<div class="card-body">
<div class="form-item">
<label class="form-label">开启 Telegram 通知</label>
<label class="switch">
<input v-model="telegramNotifyEnabled" type="checkbox" />
<span class="slider" />
</label>
</div>
<div class="form-item">
<label class="form-label">Bot Token</label>
<input
v-model="telegramBotToken"
class="form-input"
type="text"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
/>
<div class="form-hint">
Telegram 中搜索
<a href="https://t.me/BotFather" target="_blank">@BotFather</a>
创建机器人获取 Token
</div>
</div>
<div class="form-item">
<label class="form-label">Chat ID</label>
<input
v-model="telegramChatId"
class="form-input"
type="text"
placeholder="123456789"
/>
<div class="form-hint">
这是接收通知的用户 ID 或群组 ID可以先给机器人发消息然后通过 API
获取 ID或者使用
<a href="https://t.me/userinfobot" target="_blank">@userinfobot</a>
查询
</div>
</div>
<div class="card-actions" style="justify-content: space-between">
<button
class="card-button secondary"
:disabled="settingUpWebhook"
@click="doSetupWebhook"
>
<span v-if="settingUpWebhook">设置中...</span>
<span v-else>一键设置 Webhook</span>
</button>
<button
class="card-button secondary"
:disabled="testingTelegram"
@click="testTelegram"
style="margin-right: auto"
>
<span v-if="testingTelegram">发送中...</span>
<span v-else>发送测试消息</span>
</button>
<button
class="card-button"
:disabled="savingTelegram"
@click="saveTelegram"
>
<span v-if="savingTelegram">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</div>
</template>
</div>
</transition>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, type Ref } from "vue";
import {
fetchCommentSettings,
saveCommentSettings,
fetchEmailNotifySettings,
saveEmailNotifySettings,
sendTestEmail,
fetchFeatureSettings,
saveFeatureSettings,
fetchAdminDisplaySettings,
saveAdminDisplaySettings,
fetchTelegramSettings,
saveTelegramSettings,
setupTelegramWebhook,
sendTelegramTestMessage,
} from "../../api/admin";
const DEFAULT_REPLY_TEMPLATE = `<div style="background-color:#f4f4f5;padding:24px 0;">
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#2563eb,#4f46e5);">
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">评论回复 - \${postTitle}</h1>
<p style="margin:4px 0 0;font-size:12px;color:#e5e7eb;">你在文章下的评论收到了新的回复</p>
</div>
<div style="padding:24px 28px;">
<p style="margin:0 0 8px 0;font-size:14px;color:#374151;">Hi <span style="font-weight:600;">\${toName}</span></p>
<p style="margin:0 0 16px 0;font-size:14px;color:#4b5563;">
<span style="font-weight:600;">\${replyAuthor}</span> 回复了你在
<span style="font-weight:600;">《\${postTitle}》</span>
中的评论:
</p>
<div style="margin:0 0 18px 0;padding:14px 16px;border-radius:10px;background:#f3f4f6;border:1px solid #e5e7eb;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">你之前的评论</div>
<div style="font-size:14px;color:#374151;">\${parentComment}</div>
</div>
<div style="margin:0 0 24px 0;padding:14px 16px;border-radius:10px;background:#eff6ff;border:1px solid #bfdbfe;">
<div style="font-size:12px;color:#1d4ed8;margin-bottom:6px;">最新回复</div>
<div style="font-size:14px;color:#1f2937;">\${replyContent}</div>
</div>
<div style="text-align:center;margin-bottom:8px;">
<a href="\${postUrl}" style="display:inline-block;padding:10px 22px;border-radius:999px;background:#2563eb;color:#ffffff;font-size:14px;font-weight:500;text-decoration:none;">
打开文章查看完整对话
</a>
</div>
<p style="margin:0;font-size:12px;color:#9ca3af;text-align:center;">
如果按钮无法点击,可以将链接复制到浏览器中打开:<br />
<span style="word-break:break-all;color:#6b7280;">\${postUrl}</span>
</p>
</div>
<div style="padding:14px 20px;border-top:1px solid #e5e7eb;background:#f9fafb;text-align:center;">
<p style="margin:0;font-size:11px;line-height:1.6;color:#9ca3af;">
此邮件由系统自动发送,请勿直接回复。
</p>
</div>
</div>
</div>
`;
const DEFAULT_ADMIN_TEMPLATE = `<div style="background-color:#f4f4f5;padding:24px 0;">
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#0f766e,#059669);">
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">新评论提醒</h1>
<p style="margin:4px 0 0;font-size:12px;color:#d1fae5;">你的文章收到了新的评论</p>
</div>
<div style="padding:24px 28px;">
<p style="margin:0 0 10px 0;font-size:14px;color:#374151;">
<span style="font-weight:600;">\${commentAuthor}</span> 在文章
<span style="font-weight:600;">《\${postTitle}》</span>
下发表了新评论:
</p>
<div style="margin:0 0 18px 0;padding:14px 16px;border-radius:10px;background:#f9fafb;border:1px solid #e5e7eb;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">评论内容</div>
<div style="font-size:14px;color:#374151;">\${commentContent}</div>
</div>
<div style="margin:0 0 8px 0;">
<a href="\${postUrl}" style="display:inline-block;padding:10px 22px;border-radius:999px;background:#047857;color:#ffffff;font-size:14px;font-weight:500;text-decoration:none;">
打开后台查看并管理评论
</a>
</div>
<p style="margin:0;font-size:12px;color:#9ca3af;">
如果按钮无法点击,可以将链接复制到浏览器中打开:<br />
<span style="word-break:break-all;color:#6b7280;">\${postUrl}</span>
</p>
</div>
<div style="padding:14px 20px;border-top:1px solid #e5e7eb;background:#f9fafb;text-align:center;">
<p style="margin:0;font-size:11px;line-height:1.6;color:#9ca3af;">
此邮件由系统自动发送,如非本人操作可忽略本邮件。
</p>
</div>
</div>
</div>
`;
const emailGlobalEnabled = ref(true);
const commentAdminEmail = ref("");
const commentAdminBadge = ref("");
const avatarPrefix = ref("");
const commentAdminEnabled = ref(false);
const adminLayoutTitle = ref("CWD 评论系统");
const allowedDomainTags = ref<string[]>([]);
const allowedDomainInput = ref("");
const blockedIpTags = ref<string[]>([]);
const blockedIpInput = ref("");
const blockedEmailTags = ref<string[]>([]);
const blockedEmailInput = ref("");
const commentAdminKey = ref("");
const adminKeySet = ref(false);
const requireReview = ref(false);
const enableArticleLike = ref(true);
const enableCommentLike = ref(true);
const telegramBotToken = ref("");
const telegramChatId = ref("");
const telegramNotifyEnabled = ref(false);
const savingTelegram = ref(false);
const settingUpWebhook = ref(false);
const testingTelegram = ref(false);
const activeTab = ref<"comment" | "feature" | "display" | "notify">("comment");
function addAllowedDomainsFromInput() {
const raw = allowedDomainInput.value;
if (!raw) {
return;
}
const parts = raw
.split(/[,\s]+/)
.map((d) => d.trim())
.filter(Boolean);
if (parts.length === 0) {
allowedDomainInput.value = "";
return;
}
const existing = new Set(allowedDomainTags.value);
for (const part of parts) {
if (!existing.has(part)) {
allowedDomainTags.value.push(part);
existing.add(part);
}
}
allowedDomainInput.value = "";
}
function handleAllowedDomainKeyup(event: KeyboardEvent) {
if (
event.key === " " ||
event.key === "Spacebar" ||
event.key === "," ||
event.key === "" ||
event.key === "Enter"
) {
addAllowedDomainsFromInput();
}
}
function handleAllowedDomainBlur() {
addAllowedDomainsFromInput();
}
function removeAllowedDomain(domain: string) {
allowedDomainTags.value = allowedDomainTags.value.filter((d) => d !== domain);
}
function addBlockedIpFromInput() {
const raw = blockedIpInput.value;
if (!raw) {
return;
}
const parts = raw
.split(/[,\s]+/)
.map((d) => d.trim())
.filter(Boolean);
if (parts.length === 0) {
blockedIpInput.value = "";
return;
}
const existing = new Set(blockedIpTags.value);
for (const part of parts) {
if (!existing.has(part)) {
blockedIpTags.value.push(part);
existing.add(part);
}
}
blockedIpInput.value = "";
}
function handleBlockedIpKeyup(event: KeyboardEvent) {
if (
event.key === " " ||
event.key === "Spacebar" ||
event.key === "," ||
event.key === "" ||
event.key === "Enter"
) {
addBlockedIpFromInput();
}
}
function handleBlockedIpBlur() {
addBlockedIpFromInput();
}
function removeBlockedIp(ip: string) {
blockedIpTags.value = blockedIpTags.value.filter((t) => t !== ip);
}
function addBlockedEmailFromInput() {
const raw = blockedEmailInput.value;
if (!raw) {
return;
}
const parts = raw
.split(/[,\s]+/)
.map((d) => d.trim())
.filter(Boolean);
if (parts.length === 0) {
blockedEmailInput.value = "";
return;
}
const existing = new Set(blockedEmailTags.value);
for (const part of parts) {
if (!existing.has(part)) {
blockedEmailTags.value.push(part);
existing.add(part);
}
}
blockedEmailInput.value = "";
}
function handleBlockedEmailKeyup(event: KeyboardEvent) {
if (
event.key === " " ||
event.key === "Spacebar" ||
event.key === "," ||
event.key === "" ||
event.key === "Enter"
) {
addBlockedEmailFromInput();
}
}
function handleBlockedEmailBlur() {
addBlockedEmailFromInput();
}
function removeBlockedEmail(email: string) {
blockedEmailTags.value = blockedEmailTags.value.filter((t) => t !== email);
}
const savingEmail = ref(false);
const testingEmail = ref(false);
const savingComment = ref(false);
const savingFeature = ref(false);
const savingDisplay = ref(false);
const loading = ref(false);
const message = ref("");
const messageType = ref<"success" | "error">("success");
const toastMessage = ref("");
const toastType = ref<"success" | "error">("success");
const toastVisible = ref(false);
// 从本地存储加载卡片展开状态,默认只展开第一个卡片
const STORAGE_KEY = "settings-cards-expanded";
function loadCardsExpanded() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
return {
comment: parsed.comment ?? true,
display: parsed.display ?? false,
feature: parsed.feature ?? false,
email: parsed.email ?? false,
telegram: parsed.telegram ?? false,
};
}
} catch {
}
return { comment: true, display: false, feature: false, email: false, telegram: false };
}
const cardsExpanded = ref(loadCardsExpanded());
function saveCardsExpanded() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(cardsExpanded.value));
} catch {
// 忽略错误
}
}
function toggleCard(card: keyof typeof cardsExpanded.value) {
cardsExpanded.value[card] = !cardsExpanded.value[card];
saveCardsExpanded();
}
const smtpProvider = ref("qq");
const smtpHost = ref("smtp.qq.com");
const smtpPort = ref(465);
const smtpUser = ref("");
const smtpPass = ref("");
const smtpSecure = ref(true);
const templateAdmin = ref(DEFAULT_ADMIN_TEMPLATE);
const templateReply = ref(DEFAULT_REPLY_TEMPLATE);
function onProviderChange() {
if (smtpProvider.value === "qq") {
smtpHost.value = "smtp.qq.com";
smtpPort.value = 465;
smtpSecure.value = true;
}
}
function showToast(msg: string, type: "success" | "error" = "success") {
toastMessage.value = msg;
toastType.value = type;
toastVisible.value = true;
window.setTimeout(() => {
toastVisible.value = false;
}, 2000);
}
function resetTemplatesToDefault() {
templateAdmin.value = DEFAULT_ADMIN_TEMPLATE;
templateReply.value = DEFAULT_REPLY_TEMPLATE;
}
async function load() {
loading.value = true;
try {
const [commentRes, emailNotifyRes, featureRes, telegramRes, displayRes] = await Promise.all([
fetchCommentSettings(),
fetchEmailNotifySettings(),
fetchFeatureSettings(),
fetchTelegramSettings(),
fetchAdminDisplaySettings(),
]);
commentAdminEmail.value = commentRes.adminEmail || "";
commentAdminBadge.value = commentRes.adminBadge ?? "";
avatarPrefix.value = commentRes.avatarPrefix || "";
commentAdminEnabled.value = !!commentRes.adminEnabled;
const domains = Array.isArray(commentRes.allowedDomains)
? commentRes.allowedDomains
: [];
allowedDomainTags.value = domains
.map((d: string) => String(d).trim())
.filter(Boolean);
allowedDomainInput.value = "";
blockedIpTags.value = Array.isArray(commentRes.blockedIps)
? commentRes.blockedIps
: [];
blockedIpInput.value = "";
blockedEmailTags.value = Array.isArray(commentRes.blockedEmails)
? commentRes.blockedEmails
: [];
blockedEmailInput.value = "";
commentAdminKey.value = commentRes.adminKey || "";
adminKeySet.value = !!commentRes.adminKeySet;
requireReview.value = !!commentRes.requireReview;
emailGlobalEnabled.value = !!emailNotifyRes.globalEnabled;
enableArticleLike.value = featureRes.enableArticleLike;
enableCommentLike.value = featureRes.enableCommentLike;
telegramBotToken.value = telegramRes.botToken || "";
telegramChatId.value = telegramRes.chatId || "";
telegramNotifyEnabled.value = telegramRes.notifyEnabled;
adminLayoutTitle.value = displayRes.layoutTitle || "CWD 评论系统";
if (emailNotifyRes.templates) {
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;
}
if (emailNotifyRes.smtp) {
smtpHost.value = emailNotifyRes.smtp.host;
smtpPort.value = emailNotifyRes.smtp.port;
smtpUser.value = emailNotifyRes.smtp.user;
smtpPass.value = emailNotifyRes.smtp.pass;
smtpSecure.value = emailNotifyRes.smtp.secure;
if (emailNotifyRes.smtp.host === "smtp.qq.com") {
smtpProvider.value = "qq";
} else {
smtpProvider.value = "custom";
}
}
} catch (e: any) {
message.value = e.message || "加载失败";
messageType.value = "error";
} finally {
loading.value = false;
}
}
async function saveEmail() {
savingEmail.value = true;
message.value = "";
try {
const res = await saveEmailNotifySettings({
globalEnabled: emailGlobalEnabled.value,
smtp: {
host: smtpHost.value,
port: smtpPort.value,
user: smtpUser.value,
pass: smtpPass.value,
secure: smtpSecure.value,
},
templates: {
reply: templateReply.value,
admin: templateAdmin.value,
},
});
showToast(res.message || "保存成功", "success");
} catch (e: any) {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
savingEmail.value = false;
}
}
async function testEmail() {
if (!commentAdminEmail.value) {
message.value = "请先在上方“评论显示配置”中设置管理员邮箱";
messageType.value = "error";
return;
}
if (!smtpUser.value || !smtpPass.value) {
message.value = "请先填写 SMTP 账号和密码";
messageType.value = "error";
return;
}
testingEmail.value = true;
message.value = "";
try {
const res = await sendTestEmail({
toEmail: commentAdminEmail.value,
smtp: {
host: smtpHost.value,
port: smtpPort.value,
user: smtpUser.value,
pass: smtpPass.value,
secure: smtpSecure.value,
},
});
showToast(res.message || "发送成功,请查收邮件", "success");
} catch (e: any) {
// 显示详细错误信息
console.error(e);
let errorMsg = e.message || "发送失败";
// 针对 QQ 邮箱 535 错误的友好提示
if (
errorMsg.includes("535") &&
(errorMsg.includes("Login fail") || errorMsg.includes("authentication failed"))
) {
errorMsg =
"验证失败 (535):请检查 1. QQ 邮箱是否已开启 POP3/SMTP 服务2. 密码栏是否填写了“授权码”(非 QQ 密码)。";
}
message.value = errorMsg;
messageType.value = "error";
} finally {
testingEmail.value = false;
}
}
async function saveComment() {
savingComment.value = true;
message.value = "";
try {
const [commentRes] = await Promise.all([
saveCommentSettings({
adminEmail: commentAdminEmail.value,
adminBadge: commentAdminBadge.value,
avatarPrefix: avatarPrefix.value,
adminEnabled: commentAdminEnabled.value,
allowedDomains: allowedDomainTags.value,
adminKey: commentAdminKey.value || undefined,
requireReview: requireReview.value,
blockedIps: blockedIpTags.value,
blockedEmails: blockedEmailTags.value,
}),
]);
showToast(commentRes.message || "保存成功", "success");
} catch (e: any) {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
savingComment.value = false;
}
}
async function saveFeature() {
savingFeature.value = true;
message.value = "";
try {
const [featureRes] = await Promise.all([
saveFeatureSettings({
enableArticleLike: enableArticleLike.value,
enableCommentLike: enableCommentLike.value,
}),
]);
showToast(featureRes.message || "保存成功", "success");
} catch (e: any) {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
savingFeature.value = false;
}
}
async function saveDisplay() {
savingDisplay.value = true;
message.value = "";
try {
const res = await saveAdminDisplaySettings({
layoutTitle: adminLayoutTitle.value,
});
showToast(res.message || "保存成功", "success");
} catch (e: any) {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
savingDisplay.value = false;
}
}
async function saveTelegram() {
savingTelegram.value = true;
message.value = "";
try {
const res = await saveTelegramSettings({
botToken: telegramBotToken.value,
chatId: telegramChatId.value,
notifyEnabled: telegramNotifyEnabled.value,
});
showToast(res.message || "保存成功", "success");
} catch (e: any) {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
savingTelegram.value = false;
}
}
async function testTelegram() {
if (!telegramBotToken.value || !telegramChatId.value) {
showToast("请先填写 Bot Token 和 Chat ID 并保存", "error");
return;
}
testingTelegram.value = true;
try {
await saveTelegramSettings({
botToken: telegramBotToken.value,
chatId: telegramChatId.value,
notifyEnabled: telegramNotifyEnabled.value,
});
const res = await sendTelegramTestMessage();
showToast(res.message || "测试消息已发送", "success");
} catch (e: any) {
showToast(e.message || "发送失败", "error");
} finally {
testingTelegram.value = false;
}
}
async function doSetupWebhook() {
if (!telegramBotToken.value) {
showToast("请先填写 Bot Token 并保存", "error");
return;
}
settingUpWebhook.value = true;
try {
// First save settings to ensure backend has latest token
await saveTelegramSettings({
botToken: telegramBotToken.value,
chatId: telegramChatId.value,
notifyEnabled: telegramNotifyEnabled.value,
});
const res = await setupTelegramWebhook();
showToast(res.message || "Webhook 设置成功", "success");
} catch (e: any) {
showToast(e.message || "设置失败", "error");
} finally {
settingUpWebhook.value = false;
}
}
onMounted(() => {
load();
});
</script>
<style scoped lang="less">
@import "../../styles/components/setting.less";
</style>