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 PhosphorIcons from '@phosphor-icons/vue';
import './styles/theme.css'; import './styles/theme.css';
import './styles/common.less';
const app = createApp(App); const app = createApp(App);
app.use(router); 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; 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 { .visit-tabs {
display: inline-flex; display: inline-flex;
border-radius: 999px; border-radius: 999px;

View File

@@ -91,30 +91,6 @@
outline: none; 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 { .log-container {
margin-top: 16px; margin-top: 16px;
padding: 12px; padding: 12px;

View File

@@ -1,7 +1,3 @@
h1 {
color: red;
}
.page { .page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -15,6 +11,39 @@ h1 {
color: var(--text-primary); 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 { .card {
background-color: var(--bg-card); background-color: var(--bg-card);
border-radius: 6px; border-radius: 6px;
@@ -28,7 +57,6 @@ h1 {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 6px 0; padding: 6px 0;
cursor: pointer;
user-select: none; user-select: none;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
@@ -50,10 +78,6 @@ h1 {
transform: rotate(180deg); transform: rotate(180deg);
} }
.card-body {
/* padding-top: 4px; */
}
.card-subtitle { .card-subtitle {
margin: 0 0 12px; margin: 0 0 12px;
font-size: 15px; font-size: 15px;
@@ -171,30 +195,6 @@ h1 {
color: var(--text-primary); 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 { .switch {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@@ -244,6 +244,7 @@ h1 {
.card-actions { .card-actions {
display: flex; display: flex;
flex-wrap: wrap;
gap: 10px; gap: 10px;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -355,3 +356,14 @@ h1 {
opacity: 0; opacity: 0;
overflow: hidden; 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; 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) { @media (max-width: 768px) {
.stats-grid { .stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));

View File

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

View File

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