主题色优化

This commit is contained in:
BianLzhaoMin 2026-02-03 15:09:50 +08:00
parent c6e27f1af4
commit dd9838a16f
6 changed files with 624 additions and 59 deletions

View File

@ -123,6 +123,7 @@ aside {
//main-container全局样式
.app-container {
padding: 20px;
transition: background-color 0.3s ease;
}
.components-container {

View File

@ -89,49 +89,74 @@ $--color-info: #909399;
// 暗黑模式变量
html.dark {
/* 默认通用 */
--el-bg-color: #141414;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #ffffff;
--el-text-color-regular: #d0d0d0;
--el-border-color: #434343;
--el-border-color-light: #434343;
/* 默认通用 - 优化为更舒适的深色背景 */
--el-bg-color: #0f1419;
--el-bg-color-overlay: #1a1f24;
--el-text-color-primary: #e4e7ed;
--el-text-color-regular: #b3b9c4;
--el-border-color: #2d3339;
--el-border-color-light: #252a2f;
/* 主体内容区域背景 */
--main-bg-color: #0f1419;
--content-bg-color: #151a20;
/* 侧边栏 */
--sidebar-bg: #141414;
--sidebar-text: #ffffff;
--menu-hover: #2d2d2d;
--sidebar-bg: #0f1419;
--sidebar-text: #e4e7ed;
--menu-hover: #1f2529;
--menu-active-text: #{$menuActiveText};
/* 顶部导航栏 */
--navbar-bg: #141414;
--navbar-text: #ffffff;
--navbar-hover: #141414;
--navbar-bg: #0f1419;
--navbar-text: #e4e7ed;
--navbar-hover: #1a1f24;
--navbar-border: #252a2f;
/* 标签栏 */
--tags-bg: #141414;
--tags-item-bg: #1d1e1f;
--tags-item-border: #303030;
--tags-item-text: #d0d0d0;
--tags-item-hover: #2d2d2d;
--tags-close-hover: #64666a;
--tags-bg: #0f1419;
--tags-item-bg: #1a1f24;
--tags-item-border: #252a2f;
--tags-item-text: #b3b9c4;
--tags-item-hover: #1f2529;
--tags-close-hover: #4a5568;
/* splitpanes 组件暗黑模式变量 */
--splitpanes-bg: #141414;
--splitpanes-border: #303030;
--splitpanes-splitter-bg: #1d1e1f;
--splitpanes-splitter-hover-bg: #2d2d2d;
--splitpanes-bg: #0f1419;
--splitpanes-border: #252a2f;
--splitpanes-splitter-bg: #1a1f24;
--splitpanes-splitter-hover-bg: #1f2529;
/* blockquote 暗黑模式变量 */
--blockquote-bg: #1d1e1f;
--blockquote-border: #303030;
--blockquote-text: #d0d0d0;
--blockquote-bg: #1a1f24;
--blockquote-border: #252a2f;
--blockquote-text: #b3b9c4;
/* Cron 时间表达式 模式变量 */
--cron-border: #303030;
--cron-border: #252a2f;
/* splitpanes default-theme 暗黑模式变量 */
--splitpanes-default-bg: #141414;
--splitpanes-default-bg: #0f1419;
/* 搜索框暗黑模式变量 */
--search-bg: #1a1f24;
--search-border: #252a2f;
--search-text: #e4e7ed;
--search-placeholder: #6b7280;
--search-hover-bg: #1f2529;
--search-suffix-bg: #252a2f;
/* 下拉菜单暗黑模式变量 */
--dropdown-bg: #1a1f24;
--dropdown-border: #252a2f;
--dropdown-item-hover: #1f2529;
--dropdown-divider: #252a2f;
/* 主体内容区域 */
--main-bg-color: #0f1419;
--content-bg-color: #151a20;
--card-bg-color: #1a1f24;
--card-border-color: #252a2f;
/* 侧边栏菜单覆盖 */
.sidebar-container {
@ -224,5 +249,63 @@ html.dark {
background: var(--cron-border);
}
/* 主体内容区域样式覆盖 */
.app-main {
background-color: var(--main-bg-color) !important;
}
.app-container {
background-color: var(--content-bg-color);
color: var(--el-text-color-primary);
}
.components-container {
background-color: var(--content-bg-color);
color: var(--el-text-color-primary);
}
/* 卡片样式夜间模式 */
.el-card {
background-color: var(--card-bg-color) !important;
border-color: var(--card-border-color) !important;
color: var(--el-text-color-primary) !important;
}
/* 面包屑导航夜间模式 */
.el-breadcrumb {
.el-breadcrumb__inner {
color: var(--el-text-color-regular) !important;
&.is-link {
color: var(--el-text-color-regular) !important;
&:hover {
color: var(--el-color-primary) !important;
}
}
}
}
/* 侧边栏菜单项夜间模式优化 */
.sidebar-container {
.el-menu-item,
.el-sub-menu__title {
&:hover {
background-color: rgba(0, 168, 98, 0.12) !important;
}
}
.el-menu-item.is-active {
background-color: rgba(0, 168, 98, 0.18) !important;
}
}
/* 导航栏汉堡菜单夜间模式 */
.hamburger-container {
&:hover {
background: rgba(255, 255, 255, 0.05) !important;
}
}
}

View File

@ -42,6 +42,8 @@ function addIframe() {
width: 100%;
position: relative;
overflow: hidden;
background-color: var(--main-bg-color, #ffffff);
transition: background-color 0.3s ease;
}
.fixed-header + .app-main {
@ -81,10 +83,32 @@ function addIframe() {
::-webkit-scrollbar-track {
background-color: #f1f1f1;
transition: background-color 0.3s ease;
}
::-webkit-scrollbar-thumb {
background-color: #c0c0c0;
border-radius: 3px;
transition: background-color 0.3s ease;
&:hover {
background-color: #a0a0a0;
}
}
//
html.dark {
::-webkit-scrollbar-track {
background-color: var(--el-bg-color, #0f1419);
}
::-webkit-scrollbar-thumb {
background-color: #4a5568;
border-radius: 3px;
&:hover {
background-color: #5a6578;
}
}
}
</style>

View File

@ -20,7 +20,8 @@
style="width: 130px"
placeholder="搜索"
class="search-input"
@keydown.ctrl.k="handleSearch"
readonly
@click="handleSearch"
>
<template #prefix>
<el-icon class="el-input__icon"><search /></el-icon>
@ -30,18 +31,28 @@
</template>
</el-input>
<Icon size="18">
<Moon />
</Icon>
<Icon size="18">
<ScanOutline />
</Icon>
<Icon size="18">
<SunnyOutline />
</Icon>
<Icon size="18">
<NotificationsOutline />
</Icon>
<el-tooltip
:content="settingsStore.isDark ? '切换到日间模式' : '切换到夜间模式'"
effect="dark"
placement="bottom"
>
<div class="icon-wrapper theme-toggle" @click="handleThemeToggle">
<Icon size="18">
<SunnyOutline v-if="settingsStore.isDark" />
<Moon v-else />
</Icon>
</div>
</el-tooltip>
<div class="icon-wrapper">
<Icon size="18">
<ScanOutline />
</Icon>
</div>
<div class="icon-wrapper">
<Icon size="18">
<NotificationsOutline />
</Icon>
</div>
</div>
<!-- <screenfull id="screenfull" class="right-menu-item hover-effect" />
@ -62,10 +73,9 @@
</template>
<el-dropdown
placement="bottom-end"
@command="handleCommand"
class="avatar-container right-menu-item hover-effect"
placement="bottom-end"
trigger="click"
>
<div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" />
@ -137,7 +147,9 @@
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import {
Moon,
ScanOutline,
@ -149,6 +161,7 @@ import {
ChevronForward,
ExitOutline,
} from '@vicons/ionicons5'
import { initThemeTransitionCSS } from '@/utils/themeTransition'
import { Icon } from '@vicons/utils'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
@ -206,9 +219,34 @@ function toggleTheme() {
settingsStore.toggleTheme()
}
//
function handleThemeToggle(event) {
settingsStore.toggleThemeWithAnimation(event)
}
const handleSearch = () => {
console.log('handleSearch')
//
}
// Ctrl+K
const handleKeyDown = (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault()
e.stopPropagation()
handleSearch()
}
}
onMounted(() => {
window.addEventListener('keydown', handleKeyDown)
// CSS
initThemeTransitionCSS()
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
})
</script>
<style lang='scss' scoped>
@ -218,6 +256,7 @@ const handleSearch = () => {
position: relative;
background: var(--navbar-bg);
box-shadow: 0 2px 8px rgba(0, 168, 98, 0.1);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
.hamburger-container {
line-height: 46px;
@ -260,6 +299,7 @@ const handleSearch = () => {
justify-content: center;
gap: 18px;
border-right: 1px solid #e0e0e0;
transition: border-color 0.3s ease;
}
&:focus {
@ -311,18 +351,24 @@ const handleSearch = () => {
align-items: center;
border-radius: 6px;
padding: 4px;
transition: all 0.3s ease;
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
margin-right: 8px;
border-radius: 50%;
border: 2px solid transparent;
transition: border-color 0.3s ease;
}
.user-nickname {
padding: 0 4px;
font-size: 14px;
font-weight: bold;
font-weight: 500;
color: var(--navbar-text, #303133);
transition: color 0.3s ease;
}
i {
@ -349,6 +395,7 @@ const handleSearch = () => {
font-size: 14px;
font-family: 'PingFang SC', 'Microsoft YaHei', 'Arial', sans-serif;
border-bottom: 1px dashed #f0f0f0;
transition: border-color 0.3s ease;
.user-info-text {
margin-left: 6px;
@ -359,6 +406,7 @@ const handleSearch = () => {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
}
@ -367,16 +415,20 @@ const handleSearch = () => {
height: 36px;
margin-right: 8px;
border-radius: 4px;
border: 2px solid transparent;
transition: border-color 0.3s ease;
}
.user-info-text div:last-child {
color: #909599;
font-size: 12px;
}
}
.user-info-item {
padding: 4px 0 14px 0;
border-bottom: 1px dashed #f0f0f0;
transition: border-color 0.3s ease;
.user-info-item-text {
padding: 12px 20px;
@ -384,11 +436,24 @@ const handleSearch = () => {
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.6s ease;
}
.user-info-item-text:hover {
background-color: #f3f3f5;
border-radius: 4px;
transition: all 0.3s ease;
border-radius: 6px;
margin: 0 4px;
color: var(--el-text-color-regular, #606266);
:deep(svg) {
color: inherit;
transition: transform 0.3s ease;
}
&:hover {
background-color: rgba(0, 168, 98, 0.1);
color: var(--el-color-primary, #00a862);
:deep(svg:last-child) {
transform: translateX(2px);
}
}
}
}
@ -399,20 +464,26 @@ const handleSearch = () => {
justify-content: center;
.logout-item-text {
padding: 6px 16px;
padding: 8px 20px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
background-color: #f4f5f5;
border-radius: 4px;
border-radius: 6px;
color: #767c82;
border-radius: 4px;
letter-spacing: 1px;
transition: all 0.6s ease;
transition: all 0.3s ease;
font-size: 14px;
:deep(svg) {
color: inherit;
}
&:hover {
background-color: #eceded;
background-color: rgba(245, 108, 108, 0.1);
color: #f56c6c;
}
}
}
@ -427,6 +498,25 @@ const handleSearch = () => {
--el-input-border-color: transparent;
--el-input-hover-border-color: transparent;
--el-input-focus-border-color: transparent;
cursor: pointer;
transition: all 0.3s ease;
.el-input__wrapper {
cursor: pointer;
transition: all 0.3s ease;
}
.el-input__inner {
cursor: pointer;
caret-color: transparent;
transition: all 0.3s ease;
}
&:hover {
.el-input__wrapper {
background-color: rgba(0, 168, 98, 0.08);
}
}
}
.search-input-suffix-text {
@ -435,16 +525,200 @@ const handleSearch = () => {
line-height: 25px;
padding: 0 4px;
border-radius: 6px;
transition: all 0.3s ease;
font-size: 11px;
color: #909399;
}
.xicon {
cursor: pointer;
}
.xicon:hover {
color: #00a862;
background-color: rgba(0, 168, 98, 0.1);
transform: scale(1.2);
.icon-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--navbar-text, #5a5e66);
&:hover {
transform: scale(1.1);
background-color: rgba(0, 168, 98, 0.1);
.xicon {
color: #00a862;
}
}
&.theme-toggle {
position: relative;
//
:deep(svg) {
transition: transform 0.3s ease;
color: inherit;
}
&:hover {
:deep(svg) {
transform: rotate(15deg);
}
}
}
}
</style>
<style lang='scss'>
// Element Plus popper body
.el-dropdown__popper .el-dropdown__list {
padding: 8px 14px !important;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
//
html.dark {
.navbar {
background: var(--navbar-bg);
border-bottom: 1px solid var(--navbar-border, #252a2f);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.right-menu {
.icon-container {
border-right-color: var(--navbar-border, #252a2f);
}
}
.avatar-container {
.avatar-wrapper {
.user-nickname {
color: var(--navbar-text);
}
&:hover {
background-color: var(--navbar-hover);
}
}
}
}
//
.icon-container :deep(.el-input) {
--el-input-bg-color: var(--search-bg, #1a1f24);
--el-input-text-color: var(--search-text, #e4e7ed);
--el-input-placeholder-color: var(--search-placeholder, #6b7280);
.el-input__wrapper {
background-color: var(--search-bg, #1a1f24);
border: 1px solid var(--search-border, #252a2f);
box-shadow: none;
&:hover {
background-color: var(--search-hover-bg, #1f2529);
border-color: var(--el-color-primary);
}
}
.el-input__inner {
color: var(--search-text, #e4e7ed);
&::placeholder {
color: var(--search-placeholder, #6b7280);
}
}
.el-input__icon {
color: var(--search-placeholder, #6b7280);
}
}
.search-input-suffix-text {
background-color: var(--search-suffix-bg, #252a2f);
color: var(--search-placeholder, #6b7280);
border: 1px solid var(--search-border, #252a2f);
}
//
.icon-wrapper {
color: var(--navbar-text, #e4e7ed);
&:hover {
background-color: rgba(0, 168, 98, 0.15);
}
}
//
.el-dropdown__popper {
background-color: var(--dropdown-bg, #1a1f24) !important;
border: 1px solid var(--dropdown-border, #252a2f) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4) !important;
.el-dropdown-menu {
background-color: transparent;
border: none;
}
.el-dropdown-menu__item {
color: var(--el-text-color-regular, #b3b9c4) !important;
&:not(.is-disabled):hover,
&:not(.is-disabled):focus {
background-color: var(--dropdown-item-hover, #1f2529) !important;
color: var(--el-color-primary, #00a862) !important;
}
}
}
//
.user-info {
border-bottom-color: var(--dropdown-divider, #252a2f);
color: var(--el-text-color-primary, #e4e7ed);
.user-info-text {
div:first-child {
span {
color: var(--el-text-color-primary, #e4e7ed);
}
}
div:last-child {
color: var(--el-text-color-regular, #b3b9c4);
}
}
.user-avatar {
border-color: var(--dropdown-border, #252a2f);
}
}
.user-info-item {
border-bottom-color: var(--dropdown-divider, #252a2f);
.user-info-item-text {
color: var(--el-text-color-regular, #b3b9c4);
&:hover {
background-color: var(--dropdown-item-hover, #1f2529);
color: var(--el-color-primary, #00a862);
}
}
}
.logout-item {
.logout-item-text {
background-color: var(--dropdown-item-hover, #1f2529);
color: var(--el-text-color-regular, #b3b9c4);
&:hover {
background-color: rgba(245, 108, 108, 0.15);
color: #f56c6c;
}
}
}
}
</style>

View File

@ -1,6 +1,7 @@
import defaultSettings from '@/settings'
import { useDark, useToggle } from '@vueuse/core'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import { toggleThemeWithTransition } from '@/utils/themeTransition'
const isDark = useDark()
const toggleDark = useToggle(isDark)
@ -40,10 +41,17 @@ const useSettingsStore = defineStore(
this.title = title
useDynamicTitle()
},
// 切换暗黑模式
// 切换暗黑模式(无动画)
toggleTheme() {
this.isDark = !this.isDark
toggleDark()
},
// 切换暗黑模式(带动画)
toggleThemeWithAnimation(event = null) {
toggleThemeWithTransition(() => {
this.isDark = !this.isDark
toggleDark()
}, event)
}
}
})

View File

@ -0,0 +1,175 @@
/**
* 主题切换动画工具函数
* 使用 View Transitions API 实现圆形扩展/收缩动画效果
*/
/**
* 计算从指定点到视口角落的最大距离
* @param {number} x - 点击位置的 x 坐标
* @param {number} y - 点击位置的 y 坐标
* @returns {number} 最大距离像素
*/
function getDistance(x, y) {
const maxX = Math.max(x, window.innerWidth - x)
const maxY = Math.max(y, window.innerHeight - y)
return Math.sqrt(maxX * maxX + maxY * maxY)
}
/**
* 执行主题切换动画
* @param {Function} callback - 主题切换的回调函数
* @param {MouseEvent} event - 点击事件对象可选
*/
export function toggleThemeWithTransition(callback, event = null) {
// 检查浏览器是否支持 View Transitions API
if (!document.startViewTransition) {
// 不支持时直接执行回调,无动画
callback()
return
}
// 获取当前主题状态(切换前的状态)
const wasDark = document.documentElement.classList.contains('dark')
// 获取点击位置,如果没有事件则使用默认位置(右上角图标位置)
let iconX, iconY
if (event) {
iconX = event.clientX
iconY = event.clientY
} else {
// 默认位置:右上角图标区域(可以根据实际图标位置调整)
iconX = window.innerWidth - 100
iconY = 25 // 导航栏高度的一半
}
// 左下角位置
const bottomLeftX = 0
const bottomLeftY = window.innerHeight
// 计算从不同位置到视口边缘的最大距离
const iconRadius = getDistance(iconX, iconY)
const bottomLeftRadius = getDistance(bottomLeftX, bottomLeftY)
// 创建 View Transition
const transition = document.startViewTransition(() => {
callback()
})
// 设置动画样式
transition.ready.then(() => {
if (!wasDark) {
// 当前是日间模式,切换到夜间模式
// 动画效果:从左下角扩展到右上角(图标位置)
// 旧视图(日间模式)退出:淡出
document.documentElement.animate(
[
{ opacity: 1 },
{ opacity: 0 }
],
{
duration: 1000,
easing: 'ease-in',
pseudoElement: '::view-transition-old(root)'
}
)
// 新视图(夜间模式)进入:从左下角圆形扩展到图标位置
document.documentElement.animate(
[
{
clipPath: `circle(0px at ${bottomLeftX}px ${bottomLeftY}px)`,
opacity: 0
},
{
clipPath: `circle(${iconRadius}px at ${iconX}px ${iconY}px)`,
opacity: 1
}
],
{
duration: 1000,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
pseudoElement: '::view-transition-new(root)'
}
)
} else {
// 当前是夜间模式,切换到日间模式
// 动画效果:从图标位置收缩到左下角
// 旧视图(夜间模式)退出:从图标位置圆形收缩到左下角
document.documentElement.animate(
[
{
clipPath: `circle(${iconRadius}px at ${iconX}px ${iconY}px)`,
opacity: 1
},
{
clipPath: `circle(0px at ${bottomLeftX}px ${bottomLeftY}px)`,
opacity: 0
}
],
{
duration: 600,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
pseudoElement: '::view-transition-old(root)'
}
)
// 新视图(日间模式)进入:从图标位置扩展到全屏
document.documentElement.animate(
[
{
clipPath: `circle(0px at ${iconX}px ${iconY}px)`,
opacity: 0
},
{
clipPath: `circle(${iconRadius}px at ${iconX}px ${iconY}px)`,
opacity: 1
}
],
{
duration: 600,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
pseudoElement: '::view-transition-new(root)'
}
)
}
})
}
/**
* 初始化 View Transitions API CSS
* 需要在应用启动时调用
*/
export function initThemeTransitionCSS() {
// 检查是否已经添加过样式
if (document.getElementById('theme-transition-style')) {
return
}
const style = document.createElement('style')
style.id = 'theme-transition-style'
style.textContent = `
/* View Transitions API 样式 */
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* 确保动画覆盖整个视口 */
::view-transition-old(root),
::view-transition-new(root) {
width: 100%;
height: 100%;
top: 0;
left: 0;
}
`
document.head.appendChild(style)
}