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;
}
@@ -355,3 +356,14 @@ h1 {
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,13 +10,48 @@
</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 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="collapse">
<div v-show="cardsExpanded.comment" class="card-body">
<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" />
@@ -143,22 +178,24 @@
</div>
<div class="card-actions">
<button class="card-button" :disabled="savingComment" @click="saveComment">
<button
class="card-button"
:disabled="savingComment"
@click="saveComment"
>
<span v-if="savingComment">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</transition>
</div>
</template>
<template v-else-if="activeTab === 'feature'">
<div class="card">
<div class="card-header" @click="toggleCard('feature')">
<div class="card-header">
<div class="card-title">功能开关</div>
<div class="card-icon" :class="{ expanded: cardsExpanded.feature }"></div>
</div>
<transition name="collapse">
<div v-show="cardsExpanded.feature" class="card-body">
<div class="card-body">
<div class="form-item">
<label class="form-label">开启文章点赞功能</label>
<label class="switch">
@@ -175,25 +212,29 @@
<input v-model="enableCommentLike" type="checkbox" />
<span class="slider" />
</label>
<div class="form-hint">开启后评论列表中的每条评论都会显示点赞按钮</div>
<div class="form-hint">
开启后评论列表中的每条评论都会显示点赞按钮
</div>
</div>
<div class="card-actions">
<button class="card-button" :disabled="savingFeature" @click="saveFeature">
<button
class="card-button"
:disabled="savingFeature"
@click="saveFeature"
>
<span v-if="savingFeature">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</transition>
</div>
</template>
<template v-else-if="activeTab === 'display'">
<div class="card">
<div class="card-header" @click="toggleCard('display')">
<div class="card-header">
<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="card-body">
<div class="form-item">
<label class="form-label">后台标题</label>
<input
@@ -205,22 +246,24 @@
<div class="form-hint">显示在后台顶部导航栏左侧</div>
</div>
<div class="card-actions">
<button class="card-button" :disabled="savingDisplay" @click="saveDisplay">
<button
class="card-button"
:disabled="savingDisplay"
@click="saveDisplay"
>
<span v-if="savingDisplay">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</transition>
</div>
</template>
<template v-else-if="activeTab === 'notify'">
<div class="card">
<div class="card-header" @click="toggleCard('email')">
<div class="card-header">
<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="card-body">
<div class="form-item">
<label class="form-label">开启邮件通知</label>
<label class="switch">
@@ -293,7 +336,8 @@
target="_blank"
>授权码</a
>而非 QQ 密码<br />
请登录 QQ 邮箱网页版设置 - 账户中开启 POP3/SMTP 服务并生成授权码
请登录 QQ 邮箱网页版设置 - 账户中开启 POP3/SMTP
服务并生成授权码
</div>
</div>
@@ -318,8 +362,8 @@
<label class="form-label">回复通知模板 (HTML)</label>
<div class="form-hint">
可用变量${toName} (接收人昵称), ${replyAuthor} (回复人昵称),
${postTitle} (文章标题), ${postUrl} (文章链接), ${parentComment} (原评论),
${replyContent} (回复内容)
${postTitle} (文章标题), ${postUrl} (文章链接), ${parentComment}
(原评论), ${replyContent} (回复内容)
</div>
<textarea
v-model="templateReply"
@@ -357,16 +401,12 @@
</button>
</div>
</div>
</transition>
</div>
<div class="card">
<div class="card-header" @click="toggleCard('telegram')">
<div class="card-header">
<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="card-body">
<div class="form-item">
<label class="form-label">开启 Telegram 通知</label>
<label class="switch">
@@ -397,9 +437,10 @@
placeholder="123456789"
/>
<div class="form-hint">
这是接收通知的用户 ID 或群组 ID可以先给机器人发消息然后通过 API 获取
ID或者使用
<a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> 查询
这是接收通知的用户 ID 或群组 ID可以先给机器人发消息然后通过 API
获取 ID或者使用
<a href="https://t.me/userinfobot" target="_blank">@userinfobot</a>
查询
</div>
</div>
@@ -431,8 +472,10 @@
</button>
</div>
</div>
</transition>
</div>
</template>
</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) {