feat(settings): 重构设置页面为标签页布局并提取通用样式

- 将设置页面从折叠面板改为标签页布局,提升操作体验
- 创建 common.less 文件集中管理全局样式(如 toast 动画)
- 移除各组件中重复的 toast 样式定义,统一维护
- 优化设置页面的视觉结构和交互逻辑
This commit is contained in:
anghunk
2026-02-02 17:09:46 +08:00
parent 4231928f9f
commit 49693ca46e
8 changed files with 545 additions and 518 deletions

View File

@@ -5,6 +5,7 @@ import { router } from './router';
import PhosphorIcons from '@phosphor-icons/vue';
import './styles/theme.css';
import './styles/common.less';
const app = createApp(App);
app.use(router);

View File

@@ -0,0 +1,42 @@
a,
button {
-webkit-tap-highlight-color: transparent;
}
.toast {
position: fixed;
top: 20px;
left: 50%;
min-width: 220px;
max-width: 320px;
padding: 12px 16px;
border-radius: 8px;
font-size: 13px;
box-shadow: var(--shadow-card);
z-index: 1000;
opacity: 0;
transform: translate(-50%, -10px);
animation: toast-slide-in 0.3s ease-out forwards;
}
.toast-success {
background-color: var(--color-success);
color: var(--text-inverse);
}
.toast-error {
background-color: var(--color-danger);
color: var(--text-inverse);
}
@keyframes toast-slide-in {
from {
opacity: 0;
transform: translate(-50%, -10px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}

View File

@@ -205,30 +205,6 @@
height: 260px;
}
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
min-width: 220px;
max-width: 320px;
padding: 10px 14px;
border-radius: 6px;
font-size: 13px;
box-shadow: var(--shadow-card);
z-index: 1000;
}
.toast-success {
background-color: var(--color-success);
color: var(--text-inverse);
}
.toast-error {
background-color: var(--color-danger);
color: var(--text-inverse);
}
.visit-tabs {
display: inline-flex;
border-radius: 999px;

View File

@@ -91,30 +91,6 @@
outline: none;
}
/* Toast & Log styles same as before */
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
min-width: 220px;
padding: 10px 14px;
border-radius: 6px;
font-size: 13px;
box-shadow: var(--shadow-card);
z-index: 1000;
}
.toast-success {
background-color: var(--color-success);
color: var(--text-inverse);
}
.toast-error {
background-color: var(--color-danger);
color: var(--text-inverse);
}
.log-container {
margin-top: 16px;
padding: 12px;

View File

@@ -1,7 +1,3 @@
h1 {
color: red;
}
.page {
display: flex;
flex-direction: column;
@@ -15,6 +11,39 @@ h1 {
color: var(--text-primary);
}
.settings-tabs {
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
padding: 2px;
border-radius: 999px;
border: 1px solid var(--border-color);
background-color: var(--bg-input);
margin: 12px 0 16px;
overflow-x: auto;
}
.settings-tab {
border: none;
background: transparent;
padding: 8px 14px;
border-radius: 999px;
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
white-space: nowrap;
transition: background-color 0.2s ease, color 0.2s ease;
}
.settings-tab+.settings-tab {
margin-left: 4px;
}
.settings-tab-active {
background-color: var(--primary-color);
color: var(--text-inverse);
}
.card {
background-color: var(--bg-card);
border-radius: 6px;
@@ -28,7 +57,6 @@ h1 {
justify-content: space-between;
align-items: center;
padding: 6px 0;
cursor: pointer;
user-select: none;
transition: background-color 0.2s;
}
@@ -50,10 +78,6 @@ h1 {
transform: rotate(180deg);
}
.card-body {
/* padding-top: 4px; */
}
.card-subtitle {
margin: 0 0 12px;
font-size: 15px;
@@ -171,30 +195,6 @@ h1 {
color: var(--text-primary);
}
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
min-width: 220px;
max-width: 320px;
padding: 10px 14px;
border-radius: 6px;
font-size: 13px;
box-shadow: var(--shadow-card);
z-index: 1000;
}
.toast-success {
background-color: var(--color-success);
color: var(--text-inverse);
}
.toast-error {
background-color: var(--color-danger);
color: var(--text-inverse);
}
.switch {
position: relative;
display: inline-flex;
@@ -244,6 +244,7 @@ h1 {
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
}
@@ -354,4 +355,15 @@ h1 {
max-height: 0;
opacity: 0;
overflow: hidden;
}
.tab-fade-enter-active,
.tab-fade-leave-active {
transition: opacity 0.1s ease, transform 0.1s ease;
}
.tab-fade-enter-from,
.tab-fade-leave-to {
opacity: 0;
transform: translateY(4px);
}

View File

@@ -172,30 +172,6 @@
height: 260px;
}
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
min-width: 220px;
max-width: 320px;
padding: 10px 14px;
border-radius: 6px;
font-size: 13px;
box-shadow: var(--shadow-card);
z-index: 1000;
}
.toast-success {
background-color: var(--color-success);
color: var(--text-inverse);
}
.toast-error {
background-color: var(--color-danger);
color: var(--text-inverse);
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));

View File

@@ -354,9 +354,8 @@ function closeVersionModal() {
}
</script>
<style scoped lang="less">
<style lang="less">
@import "../../styles/layout.less";
.modal-overlay {
position: fixed;
inset: 0;

View File

@@ -10,429 +10,472 @@
</div>
<div v-if="loading" class="page-hint">加载中...</div>
<div v-else>
<div class="card">
<div class="card-header" @click="toggleCard('comment')">
<div class="card-title">评论显示配置</div>
<div class="card-icon" :class="{ expanded: cardsExpanded.comment }"></div>
</div>
<transition name="collapse">
<div v-show="cardsExpanded.comment" 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"
<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
>
<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="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="allowedDomainInput"
class="tag-input-input"
@keyup="handleAllowedDomainKeyup"
@blur="handleAllowedDomainBlur"
v-model="commentAdminKey"
class="form-input"
placeholder="输入密钥以设置或修改"
autocomplete="new-password"
/>
</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 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>
</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"
<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
>
<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 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>
<div class="card-actions">
<button class="card-button" :disabled="savingComment" @click="saveComment">
<span v-if="savingComment">保存中...</span>
<span v-else>保存</span>
</button>
</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>
</div>
</transition>
</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="card">
<div class="card-header" @click="toggleCard('feature')">
<div class="card-title">功能开关</div>
<div class="card-icon" :class="{ expanded: cardsExpanded.feature }"></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 name="collapse">
<div v-show="cardsExpanded.feature" 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>
</transition>
</div>
<div class="card">
<div class="card-header" @click="toggleCard('display')">
<div class="card-title">后台显示设置</div>
<div class="card-icon" :class="{ expanded: cardsExpanded.display }"></div>
</div>
<transition name="collapse">
<div v-show="cardsExpanded.display" 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>
</transition>
</div>
<div class="card">
<div class="card-header" @click="toggleCard('email')">
<div class="card-title">通知邮箱设置</div>
<div class="card-icon" :class="{ expanded: cardsExpanded.email }"></div>
</div>
<transition name="collapse">
<div v-show="cardsExpanded.email" 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>
</transition>
</div>
<div class="card">
<div class="card-header" @click="toggleCard('telegram')">
<div class="card-title">Telegram 通知设置</div>
<div class="card-icon" :class="{ expanded: cardsExpanded.telegram }"></div>
</div>
<transition name="collapse">
<div v-show="cardsExpanded.telegram" 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>
</transition>
</div>
</transition>
</div>
</div>
</template>
@@ -554,6 +597,8 @@ 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) {