feat(ui): 动态设置侧边栏背景颜色,增加渐变色选择器

This commit is contained in:
秦白起 2025-01-23 18:05:21 +08:00
parent 8994e3ad3e
commit b51e370a4f
8 changed files with 184 additions and 106 deletions

View File

@ -47,6 +47,7 @@
"vue-json-pretty": "2.4.0",
"vue-router": "4.4.5",
"vue-types": "5.1.3",
"vue3-colorpicker": "^2.3.0",
"vxe-table": "4.5.22"
},
"devDependencies": {

View File

@ -14,7 +14,7 @@
-webkit-transition: width 0.28s;
transition: width 0.28s;
width: $base-sidebar-width !important;
background-color: $base-menu-background;
background: $gradient-menu-background; // 将渐变色应用到侧边栏容器
height: 100%;
position: fixed;
font-size: 0;
@ -70,6 +70,7 @@
border: none;
height: 100%;
width: 100% !important;
background: transparent !important; // 让菜单背景透明继承父容器渐变色
}
.el-menu-item,
@ -77,6 +78,7 @@
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
background: transparent !important; // 让菜单项背景透明继承父容器渐变色
}
.el-menu-item .el-menu-tooltip__trigger {
@ -87,13 +89,13 @@
.theme-dark .sub-menu-title-noDropdown,
.theme-dark .el-sub-menu__title {
&:hover {
background-color: $base-sub-menu-title-hover !important;
background-color: rgba(255, 255, 255, 0.1) !important; // 使用半透明背景色
}
}
.sub-menu-title-noDropdown,
.el-sub-menu__title {
&:hover {
background-color: rgba(0, 0, 0, 0.05) !important;
background-color: rgba(0, 0, 0, 0.05) !important; // 使用半透明背景色
}
}
@ -105,31 +107,29 @@
& .el-sub-menu .el-menu-item {
min-width: $base-sidebar-width !important;
&:hover {
background-color: rgba(0, 0, 0, 0.1) !important;
background-color: rgba(0, 0, 0, 0.1) !important; // 使用半透明背景色
}
}
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: $base-sub-menu-background !important;
background-color: transparent !important; // 让菜单项背景透明继承父容器渐变色
&:hover {
background-color: $base-sub-menu-hover !important;
background-color: rgba(255, 255, 255, 0.1) !important; // 使用半透明背景色
}
}
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
& .theme-dark .el-menu-item {
&:hover {
// you can use $sub-menuHover
background-color: $base-menu-hover !important;
background-color: rgba(255, 255, 255, 0.1) !important; // 使用半透明背景色
}
}
& .nest-menu .el-sub-menu > .el-sub-menu__title,
& .el-menu-item {
&:hover {
// you can use $sub-menuHover
background-color: rgba(0, 0, 0, 0.04) !important;
background-color: rgba(0, 0, 0, 0.04) !important; // 使用半透明背景色
}
}
}
@ -229,4 +229,4 @@
margin-right: 16px;
}
}
}
}

View File

@ -113,6 +113,8 @@ $--color-danger: #f56c6c;
$--color-info: #909399;
$base-sidebar-width: 200px;
// 默认渐变色
$gradient-menu-background: linear-gradient(to top, #12c2e9, #c471ed, #f64f59);
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass

View File

@ -1,32 +1,17 @@
<template>
<el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
<h3 class="drawer-title">主题风格设置</h3>
<div class="setting-drawer-block-checbox">
<div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.DARK)">
<img src="@/assets/images/dark.svg" alt="dark" />
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
<i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</i>
</div>
</div>
<div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)">
<img src="@/assets/images/light.svg" alt="light" />
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
<i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<path
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
/>
</svg>
</i>
</div>
</div>
<div class="drawer-item">
<span>侧边栏颜色</span>
<span class="comp-style">
<!-- https://github.com/aesoper101/vue3-colorpicker/blob/main/README.ZH-cn.md -->
<ColorPicker
v-model:pureColor="pureColor"
useType="both"
format="hex6"
@update:pureColor="onPureColorChange"
@update:gradientColor="onGradientColorChange"
/>
</span>
</div>
<div class="drawer-item">
<span>主题颜色</span>
@ -93,8 +78,11 @@ import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
import { handleThemeStyle } from '@/utils/theme';
import { handleSideThemeStyle } from '@/utils/sideTheme'; //
import { SideThemeEnum } from '@/enums/SideThemeEnum';
import defaultSettings from '@/settings';
import { ColorPicker } from 'vue3-colorpicker';
import 'vue3-colorpicker/style.css';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const appStore = useAppStore();
@ -105,8 +93,11 @@ const showSettings = ref(false);
const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
// f40002
const predefineColors = ref(['#409EFF', '#ffffff', '#1d1e1f', '#f40002', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
//
const pureColor = ref('#409EFF'); //
// const gradientColor = ref('linear-gradient(to top, #12c2e9, #c471ed, #f64f59)'); //
//
const isDark = useDark({
storageKey: 'useDarkKey',
@ -135,19 +126,28 @@ const dynamicTitleChange = () => {
useDynamicTitle();
};
//
const onPureColorChange = (color: string) => {
sideTheme.value = color;
sideThemeChange(color);
};
//
const onGradientColorChange = (gradient: string) => {
sideTheme.value = gradient;
sideThemeChange(gradient);
};
//
const sideThemeChange = (val: string) => {
settingsStore.sideTheme = val;
handleSideThemeStyle(val);
};
//
const themeChange = (val: string) => {
settingsStore.theme = val;
handleThemeStyle(val);
};
const handleTheme = (val: string) => {
sideTheme.value = val;
if (isDark.value && val === SideThemeEnum.LIGHT) {
//
settingsStore.sideTheme = SideThemeEnum.DARK;
return;
}
settingsStore.sideTheme = val;
};
const saveSetting = () => {
proxy?.$modal.loading('正在保存到本地,请稍候...');
const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
@ -186,45 +186,6 @@ defineExpose({
font-size: 14px;
}
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.custom-img {
width: 48px;
height: 38px;
border-radius: 5px;
box-shadow: 1px 1px 2px #898484;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
}
}
}
.drawer-item {
padding: 12px 0;

View File

@ -1,19 +1,15 @@
<template>
<div
class="sidebar-logo-container"
:class="{ collapse: collapse }"
:style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
>
<div class="sidebar-logo-container" :class="{ collapse: collapse }" :style="{ backgroundColor: bgColor }">
<transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
<h1 v-else class="sidebar-title" :style="{ color: textColor }">
{{ title }}
</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
<h1 class="sidebar-title" :style="{ color: textColor }">
{{ title }}
</h1>
</router-link>
@ -22,7 +18,6 @@
</template>
<script setup lang="ts">
import variables from '@/assets/styles/variables.module.scss';
import logo from '@/assets/logo/logo.png';
import useSettingsStore from '@/store/modules/settings';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@ -37,6 +32,33 @@ defineProps({
const title = ref('RuoYi-Vue-Plus');
const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme);
//
const bgColor = computed(() => (sideTheme.value.startsWith('linear-gradient') ? sideTheme.value : settingsStore.sideTheme));
//
const textColor = computed(() => {
// bgColor
if (bgColor.value.startsWith('linear-gradient')) {
return '#ffffff';
}
//
const isDarkColor = isColorDark(bgColor.value);
return isDarkColor ? '#ffffff' : '#606266FF';
});
//
const isColorDark = (color: string): boolean => {
// RGB
const hex = color.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Y = 0.299*R + 0.587*G + 0.114*B
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
console.log('Brightness:', brightness); //
// 128
return brightness < 192;
};
</script>
<style lang="scss" scoped>
@ -54,7 +76,6 @@ const sideTheme = computed(() => settingsStore.sideTheme);
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
text-align: center;
overflow: hidden;
@ -72,7 +93,6 @@ const sideTheme = computed(() => settingsStore.sideTheme);
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;

View File

@ -1,12 +1,12 @@
<template>
<div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: bgColor }">
<div :class="{ 'has-logo': showLogo }" :style="{ background: bgColor, '--text-color': textColor }">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<transition :enter-active-class="proxy?.animate.menuSearchAnimate.enter" mode="out-in">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="bgColor"
:style="{ background: bgColor }"
:text-color="textColor"
:unique-opened="true"
:active-text-color="theme"
@ -23,7 +23,6 @@
<script setup lang="ts">
import Logo from './Logo.vue';
import SidebarItem from './SidebarItem.vue';
import variables from '@/assets/styles/variables.module.scss';
import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
@ -37,19 +36,43 @@ const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const sidebarRouters = computed<RouteRecordRaw[]>(() => permissionStore.getSidebarRoutes());
const showLogo = computed(() => settingsStore.sidebarLogo);
//
const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme);
const isCollapse = computed(() => !appStore.sidebar.opened);
const activeMenu = computed(() => {
const { meta, path } = route;
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
});
const bgColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground));
const textColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuColor : variables.menuLightColor));
//
const bgColor = computed(() => (sideTheme.value.startsWith('linear-gradient') ? sideTheme.value : settingsStore.sideTheme));
const textColor = computed(() => {
// bgColor
if (bgColor.value.startsWith('linear-gradient')) {
return '#ffffff';
}
//
const isDarkColor = isColorDark(bgColor.value);
return isDarkColor ? '#ffffff' : '#606266FF';
});
//
const isColorDark = (color: string): boolean => {
// RGB
const hex = color.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Y = 0.299*R + 0.587*G + 0.114*B
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
console.log('Brightness:', brightness); //
// 128
return brightness < 192;
};
</script>

View File

@ -6,12 +6,17 @@ const setting: DefaultSettings = {
*/
title: import.meta.env.VITE_APP_TITLE,
/**
*
*/
theme: '#409EFF',
/**
* theme-darktheme-light
*
* 使 CSS
*/
sideTheme: 'theme-dark',
// sideTheme: 'linear-gradient(180deg, #2af598 0%, #009efd 100%)',
sideTheme: 'linear-gradient(to top, #12c2e9, #c471ed, #f64f59)',
/**
*
*/
@ -50,13 +55,19 @@ const setting: DefaultSettings = {
*/
errorLog: 'production',
/** 是否启用动画 */
animationEnable: false,
/** 是否启用暗黑模式 */
dark: false,
/** 默认语言 */
language: LanguageEnum.zh_CN,
/** 组件大小 */
size: 'default',
/** 布局模式 */
layout: ''
};
export default setting;

60
src/utils/sideTheme.ts Normal file
View File

@ -0,0 +1,60 @@
// 处理侧边栏颜色样式
export const handleSideThemeStyle = (sideTheme: string) => {
// 判断是否为渐变色
if (sideTheme.startsWith('linear-gradient') || sideTheme.startsWith('radial-gradient')) {
// 如果是渐变色,直接应用到侧边栏背景
document.documentElement.style.setProperty('--side-theme-background', sideTheme);
return; // 渐变色不需要生成浅色和深色变体
}
// 如果是纯色,继续处理浅色和深色变体
// document.documentElement.style.setProperty('--side-theme-background', sideTheme);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--side-theme-light-${i}`, `${getLightColor(sideTheme, i / 10)}`);
}
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--side-theme-dark-${i}`, `${getDarkColor(sideTheme, i / 10)}`);
}
};
// hex颜色转rgb颜色
export const hexToRgb = (str: string): string[] => {
str = str.replace('#', '');
const hexs = str.match(/../g);
for (let i = 0; i < 3; i++) {
if (hexs) {
hexs[i] = String(parseInt(hexs[i], 16));
}
}
return hexs ? hexs : [];
};
// rgb颜色转Hex颜色
export const rgbToHex = (r: string, g: string, b: string) => {
const hexs = [Number(r).toString(16), Number(g).toString(16), Number(b).toString(16)];
for (let i = 0; i < 3; i++) {
if (hexs[i].length == 1) {
hexs[i] = `0${hexs[i]}`;
}
}
return `#${hexs.join('')}`;
};
// 变浅颜色值
export const getLightColor = (color: string, level: number) => {
const rgb = hexToRgb(color);
for (let i = 0; i < 3; i++) {
const s = (255 - Number(rgb[i])) * level + Number(rgb[i]);
rgb[i] = String(Math.floor(s));
}
return rgbToHex(rgb[0], rgb[1], rgb[2]);
};
// 变深颜色值
export const getDarkColor = (color: string, level: number) => {
const rgb = hexToRgb(color);
for (let i = 0; i < 3; i++) {
rgb[i] = String(Math.floor(Number(rgb[i]) * (1 - level)));
}
return rgbToHex(rgb[0], rgb[1], rgb[2]);
};