Vue3.5 企业级管理系统实战(十四):动态主题切换

news2025/4/15 8:22:15

动态主题切换是针对用户体验的常见的功能之一,我们可以自己实现如暗黑模式、明亮模式的切换,也可以利用 Element Plus 默认支持的强大动态主题方案实现。这里我们探讨的是后者通过 CSS 变量设置的方案

1 组件准备

1.1 修改 Navbar 组件

在 src/layout/components/Navbar.vue 中添加设置按钮,代码如下:

//src/layout/components/Navbar.vue
<template>
  <div class="navbar" flex>
    <hamburger
      @toggleCollapse="toggleSidebar"
      :collapse="sidebar.opened"
    ></hamburger>
    <BreadCrumb></BreadCrumb>

    <div flex justify-end flex-1 items-center mr-20px>
      <screenfull mx-5px></screenfull>
      <el-tooltip content="ChangeSize" placement="bottom">
        <size-select></size-select>
      </el-tooltip>
      <svg-icon
        icon-name="ant-design:setting-outlined"
        size-2em
        @click="openShowSetting"
      ></svg-icon>
    </div>
  </div>
</template>

<style scoped lang="scss">
.navbar {
  @apply h-[var(--navbar-height)];
}
</style>

<script lang="ts" setup>
import { useAppStore } from "@/stores/app";
// 在解构的时候要考虑值是不是对象,如果非对象解构出来就 丧失响应式了
const { toggleSidebar, sidebar } = useAppStore();

const emit = defineEmits<{
  (event: "showSetting", isShow: boolean): void;
}>();

const openShowSetting = () => {
  emit("showSetting", true);
};
</script>

1.2 封装 RightPanel 组件

我们希望点击设置按钮后,右侧出现抽屉面板。在 src/components 下新建 RightPanel,然后新建 index.vue 文件,代码如下:

//src/components/RightPanel/index.vue 
<template>
  <el-drawer
    :model-value="modelValue"
    :direction="direction"
    :title="title"
    @close="handleClose"
  >
    <slot></slot>
  </el-drawer>
</template>

<script lang="ts" setup>
import type { DrawerProps } from "element-plus";
import type { PropType } from "vue";
defineProps({
  modelValue: {
    type: Boolean,
    default: false
  },
  direction: {
    type: String as PropType<DrawerProps["direction"]>,
    default: "rtl"
  },
  title: {
    type: String,
    default: ""
  }
});
const emit = defineEmits(["update:modelValue"]);
const handleClose = () => {
  emit("update:modelValue", false);
};
</script>

1.3 封装 ThemePicker 组件

在 src/components 下新建 ThemePicker/index.vue 用来选择主题颜色,代码如下:

//src/components/ThemeOicker/index.vue
<template>
  <el-color-picker v-model="theme" :predefine="predefineColors" />
</template>

<script lang="ts" setup>
import { useSettingStore } from "@/stores/settings";

const store = useSettingStore();

const theme = ref(store.settings.theme);
const predefineColors = [
  "#ff4500",
  "#ff8c00",
  "#ffd700",
  "#90ee90",
  "#00ced1",
  "#1e90ff",
  "#c71585"
];
watch(theme, (newValue) => {
  store.changeSetting({ key: "theme", value: newValue });
});
</script>

 注:useSettingStore 会在后文中说明。

1.4 封装 Settings 组件

在 src/layout/components 下新建 Settings/index.vue,用来引入系统配置相关的组件,代码如下:

//src/layout/components/Settings/index.vue
<template>
  <div class="drawer-item">
    <span>主题色</span>
    <theme-picker></theme-picker>
  </div>
  <!--后面可以放其他配置项-->
</template>

1.5 页面引用

修改 src/layout/index.vue,引入 RightPanel 组件,并修改 navbar 组件自定义事件,代码如下:

//src/layout/index.vue
<template>
  <div class="app-wrapper">
    <div class="sidebar-container">
      <sidebar></sidebar>
    </div>
    <div class="main-container">
      <div class="header">
        <!--  上边包含收缩的导航条 -->
        <navbar @showSetting="openSetting"></navbar>
        <tags-view></tags-view>
      </div>
      <div class="app-main">
        <app-main></app-main>
      </div>
    </div>

    <right-panel v-model="setting" title="设置">
      <!-- 设置功能 -->
      <Settings></Settings>
    </right-panel>
  </div>
</template>

<script lang="ts" setup>
const setting = ref(false);
const openSetting = () => {
  setting.value = true;
};
</script>
<style lang="scss" scoped>
.app-wrapper {
  @apply flex w-full h-full;
  .sidebar-container {
    // 跨组件设置样式
    @apply bg-[var(--menu-bg)];
    :deep(.sidebar-container-menu:not(.el-menu--collapse)) {
      @apply w-[var(--sidebar-width)];
    }
  }
  .main-container {
    @apply flex flex-col flex-1 overflow-hidden;
  }
  .header {
    @apply h-84px;
    .navbar {
      @apply h-[var(--navbar-height)] bg-yellow;
    }
    .tags-view {
      @apply h-[var(--tagsview-height)] bg-blue;
    }
  }
  .app-main {
    @apply bg-cyan;
    min-height: calc(100vh - var(--tagsview-height) - var(--navbar-height));
  }
}
</style>

2 主题设置方法

2.1 修改 Style

在 src/style 下,添加默认主题色,分别修改 variables.module.scss 和 variables.module.scss.d.ts,代码如下:

//variables.module.scss
$sideBarWidth: 210px;
$navBarHeight: 50px;
$tagsViewHeight: 34px;

// 导航颜色
$menuText: #bfcbd9;
// 导航激活的颜色
$menuActiveText: #409eff;
// 菜单背景色
$menuBg: #304156;
$theme: #409eff;

:export {
  menuText: $menuText;
  menuActiveText: $menuActiveText;
  menuBg: $menuBg;
  theme: $theme;
}
//variables.module.scss.d.ts
interface IVaraibles {
  menuText: string;
  menuActiveText: string;
  menuBg: string;
  theme: string;
  navBarHeight: string;
  tagsViewHeight: string;
}

export const varaibles: IVaraibles;
export default varaibles;

2.2 添加 settings Store

在 src/stores 下新建 settings.ts,代码如下:

//src/stores/settings.ts
import varaibles from "@/style/variables.module.scss";

// 定义一个名为 "setting" 的 Vuex 存储,用于管理应用的设置
export const useSettingStore = defineStore(
  "setting",
  () => {
    // 如果选择的是同样的主题颜色,就不更改,节省
    const settings = reactive({
      theme: varaibles.theme, //当前选择的颜色
      originalTheme: "" //正在应用的颜色
    });
    type ISetting = typeof settings;

    // 定义一个方法来更改设置
    const changeSetting = <T extends keyof ISetting>({
      key,
      value
    }: {
      key: T;
      value: ISetting[T];
    }) => {
      settings[key] = value;
    };
    return { changeSetting, settings };
  },
  {
    persist: {
      storage: sessionStorage, // 使用 sessionStorage 作为持久化存储
      pick: ["settings.theme"] // 持久化 settings 对象中的 theme 属性
    }
  }
);

2.3 安装 css-color-function

查看 Element Plus 源码(如下图,详细代码感兴趣的可以自己去看看源码),可以发现它是用 --el-color-#{$type} 、--el-color-#{$type}-light-{$i} 来设置基础颜色的,我们只需要修改这两个变量对应的颜色,就可以更换主题了。

要生成以上两个变量的形式,我们需要借助一个库 css-color-function,使用 pnpm 安装,代码如下:

pnpm i css-color-function

在 src/vite-env.d.ts中添加声明,增加 ts 提示,代码如下:

/// <reference types="vite/client" />

declare module "element-plus/dist/locale/en.mjs";
declare module "element-plus/dist/locale/zh-cn.mjs";

declare module "css-color-function" {
  export function convert(color: string): string;
}

2.4 color 公共方法

在 src/utils 下新建 color.ts,封装生成和设置主题颜色的方法,代码如下:

//src/utils/color.ts
// 引入 css-color-function 库,该库用于处理 CSS 颜色函数表达式
import cssFunc from "css-color-function";

// 定义一个对象 formula,用于存储颜色生成公式
// 每个属性名代表生成的颜色名称,属性值是一个 CSS 颜色函数表达式
// "xxx" 是一个占位符,后续会被实际的主色调替换
// tint 函数用于将颜色变亮,括号内的百分比表示变亮的程度
const formula: { [prop: string]: string } = {
  "primary-light-1": "color(xxx tint(10%))",
  "primary-light-2": "color(xxx tint(20%))",
  "primary-light-3": "color(xxx tint(30%))",
  "primary-light-4": "color(xxx tint(40%))",
  "primary-light-5": "color(xxx tint(50%))",
  "primary-light-6": "color(xxx tint(60%))",
  "primary-light-7": "color(xxx tint(70%))",
  "primary-light-8": "color(xxx tint(80%))",
  "primary-light-9": "color(xxx tint(90%))"
};

// 定义一个函数 generateColors,用于根据主色调生成一系列变亮的颜色
// 参数 primary 是传入的主色调,通常是一个有效的 CSS 颜色值
const generateColors = (primary: string) => {
  // 定义一个空对象 colors,用于存储生成的颜色
  const colors: Record<string, string> = {};
  // 遍历 formula 对象的每个键值对
  Object.entries(formula).forEach(([key, v]) => {
    // 将颜色公式中的占位符 "xxx" 替换为实际的主色调
    const value = v.replace(/xxx/g, primary);
    // 使用 cssFunc.convert 方法将颜色函数表达式转换为实际的 CSS 颜色值
    // 并将生成的颜色存储到 colors 对象中,键为颜色名称,值为实际颜色值
    colors[key] = cssFunc.convert(value);
  });
  // 返回生成的颜色对象
  return colors;
};

// 定义一个函数 setColors,用于将生成的颜色设置为 HTML 根元素的自定义属性
// 参数 colors 是一个对象,包含颜色名称和对应的实际颜色值
const setColors = (colors: Record<string, string>) => {
  // 获取 HTML 根元素
  const el = document.documentElement;
  // 遍历 colors 对象的每个键值对
  Object.entries(colors).forEach(([key, value]) => {
    // 使用 el.style.setProperty 方法将颜色值设置为根元素的自定义属性
    // 自定义属性的名称为 --el-color- 加上颜色名称
    el.style.setProperty(`--el-color-${key}`, value);
  });
};

// 导出 generateColors 和 setColors 函数,以便在其他模块中使用
export { generateColors, setColors };

‌2.5 设置主题

在 src 下新建 hooks 文件夹,新建 useGenerateTheme.ts 文件,用于监控主题颜色变化并设置主题颜色,代码如下:

//src/hooks/useGenerateTheme.ts 
// 从 @/stores/settings 模块中导入 useSettingStore 函数,该函数用于获取设置状态的存储实例
import { useSettingStore } from "@/stores/settings";
// 从 @/utils/color 模块中导入 generateColors 和 setColors 函数
// generateColors 用于根据主色调生成一系列变亮的颜色
// setColors 用于将生成的颜色设置为 HTML 根元素的自定义属性
import { generateColors, setColors } from "@/utils/color";

// 定义一个组合式函数 useGenerateTheme,用于处理主题生成和更新逻辑
export const useGenerateTheme = () => {
  // 通过调用 useSettingStore 函数获取设置状态的存储实例
  const store = useSettingStore();

  const theme = computed(() => store.settings.theme);
  const originalTheme = computed(() => store.settings.originalTheme);

  // 使用 watchEffect 函数创建一个副作用函数,该函数会在其依赖项发生变化时自动执行
  watchEffect(() => {
    // 检查当前的主题值 theme.value 是否与原始主题值 originalTheme.value 不同
    if (theme.value !== originalTheme.value) {
      // 如果主题值发生了变化,生成新的颜色对象
      const colors = {
        // 将当前主题颜色作为主色调
        primary: theme.value,
        // 调用 generateColors 函数,根据当前主题颜色生成一系列变亮的颜色
        // 并将这些颜色合并到 colors 对象中
        ...generateColors(theme.value)
      };

      // 调用 setColors 函数,将生成的颜色对象应用到 HTML 根元素的自定义属性上
      // 从而实现主题颜色的更新
      setColors(colors);

      // 调用存储实例的 changeSetting 方法,更新存储中的原始主题值
      // 使其与当前主题值保持一致
      store.changeSetting({ key: "originalTheme", value: theme.value });
    }
  });

  // 此函数的主要目的是生成主题颜色并将其应用到 HTML 根元素上
  // 以实现主题的动态更新
};

2.6 页面引入

在 App.vue 中引入 useGenerateTheme.ts 进行应用,代码如下:

//src/App.vue
<template>
  <el-config-provider :size="store.size" :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

<script lang="ts" setup>
import { useAppStore } from "./stores/app";
import en from "element-plus/dist/locale/en.mjs";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import { useGenerateTheme } from "@/hooks/useGenerateTheme";
const language = ref("zh-cn");
const locale = computed(() => (language.value === "zh-cn" ? zhCn : en));

const store = useAppStore();

useGenerateTheme(); // watch主题
</script>

3 组件应用

3.1 Element 组件

完成文章前面的步骤后,Element 组件都可以在选择主题后,自动变化主题颜色了。效果如下:

3.2 自定义组件

自定义组件要使用主题切换,需要在组件中进行添加,示例如下:

示例1:

修改 TagsView 组件,添加主题部分,因原代码太长,省略其他后的代码如下:

<template>
  <div class="tags-view-container">
    <el-scrollbar w-full whitespace-nowrap>
      <router-link
        class="tags-view-item"
        v-for="(tag, index) in visitedViews"
        :class="{
          active: isActive(tag)
        }"
        :style="{
          backgroundColor: isActive(tag) ? theme : '',
          borderColor: isActive(tag) ? theme : ''
        }"
        :key="index"
        :to="{ path: tag.path, query: tag.query }"
      >
        <!-- 省略其他代码 -->
      </router-link>
    </el-scrollbar>
  </div>
</template>

<script lang="ts" setup>
import { useTagsView } from "@/stores/tagsView";
import type {
  RouteLocationNormalizedGeneric,
  RouteRecordRaw
} from "vue-router";
import { join } from "path-browserify";
import { routes } from "@/router/index"; //从应用的路由配置文件中导入了所有的路由定义
import { useSettingStore } from "@/stores/settings";
//...省略其他代码...
const isActive = (tag: RouteLocationNormalizedGeneric) => {
  return tag.path === route.path;
};
//...省略其他代码...
const settingsStore = useSettingStore();
const theme = computed(() => settingsStore.settings.theme);
</script>

<style scoped>
//...省略其他代码...
</style>

示例2:

修改 Sidebar 组件,添加主题部分,完整代码如下:

//src/layout/components/Sidebar/index.vue
<template>
  <div>
    <el-menu
      class="sidebar-container-menu"
      router
      :default-active="defaultActive"
      :background-color="varaibles.menuBg"
      :text-color="varaibles.menuText"
      :active-text-color="theme"
      :collapse="sidebar.opened"
    >
      <sidebar-item
        v-for="route in routes"
        :key="route.path"
        :item="route"
        :base-path="route.path"
      />
      <!-- 增加父路径,用于el-menu-item渲染的时候拼接 -->
    </el-menu>
  </div>

  <!-- :collapse="true" -->
</template>

<script lang="ts" setup>
import { useAppStore } from "@/stores/app";
import varaibles from "@/style/variables.module.scss";
import { routes } from "@/router";
import { useSettingStore } from "@/stores/settings";

const route = useRoute();

const { sidebar } = useAppStore();

const defaultActive = computed(() => {
  // .....
  return route.path;
});

const settingsStore = useSettingStore();
const theme = computed(() => settingsStore.settings.theme);
</script>

<style scoped></style>

完成后,页面效果如下:

以上,就是主题切换的全部内容。

下一篇将继续探讨 tagsView 切换设置,敬请期待~

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2334230.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决Ubuntu20.04安装ROS2的问题(操作记录)

一、ROS 系统安装版本选择 每版的Ubuntu系统版本都有与之对应ROS版本&#xff0c;每一版ROS都有其对应版本的Ubuntu版本&#xff0c;切记不可随便装。ROS 和Ubuntu之间的版本对应关系如下&#xff1a;&#xff08; 可以从这个网站查看ROS2的各个发行版本的介绍信息。&#xff…

C# 设置Excel中文本的对齐方式、换行、和旋转

在 Excel 中&#xff0c;对齐、换行和旋转是用于设置单元格内容显示方式的功能。合理的设置这些文本选项可以帮助用户更好地组织和展示 Excel 表格中的数据&#xff0c;使表格更加清晰、易读&#xff0c;提高数据的可视化效果。本文将介绍如何在.NET 程序中通过C# 设置Excel单元…

RPA VS AI Agent

图片来源网络 RPA&#xff08;机器人流程自动化&#xff09;和AI Agent&#xff08;人工智能代理&#xff09;在自动化和智能化领域各自扮演着重要角色&#xff0c;但它们之间存在显著的区别。以下是对两者区别的详细分析&#xff1a; 一、定义与核心功能 RPA&#xff08;机…

uniapp大文件分包

1. 在pages.json中配置 "subPackages":[{"root":pagesUser,"pages":[{"path":mine/xxx,"style":xxx },{"path":mine/xxx,"style":xxx}]},{"root":pagesIndex,"pages":[{"p…

Spark-core编程

sortByKey 函数说明 join 函数说明 leftOuterJoin 函数说明 cogroup 函数说明 RDD行动算子&#xff1a; 行动算子就是会触发action的算子&#xff0c;触发action的含义就是真正的计算数据。 reduce 函数说明 collect 函数说明 foreach 函数说明 count 函数说明 first …

2025年的Android NDK 快速开发入门

十年前写过一篇介绍NDK开发的文章《Android实战技巧之二十三&#xff1a;Android Studio的NDK开发》&#xff0c;今天看来已经发生了很多变化&#xff0c;NDK开发变得更加容易了。下面就写一篇当下NDK开发快速入门。 **原生开发套件 (NDK) **是一套工具&#xff0c;使开发者能…

基于springboot的“嗨玩旅游网站”的设计与实现(源码+数据库+文档+PPT)

基于springboot的“嗨玩旅游网站”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;springboot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统功能结构图 局部E-R图 系统首页界面 系统注册…

React 之 Redux 第三十一节 useDispatch() 和 useSelector()使用以及详细案例

使用 Redux 实现购物车案例 由于 redux 5.0 已经将 createStore 废弃&#xff0c;我们需要先将 reduxjs/toolkit 安装一下&#xff1b; yarn add reduxjs/toolkit// 或者 npm install reduxjs/toolkit使用 vite 创建 React 项目时候 配置路径别名 &#xff1a; // 第一种写法…

Llama 4全面评测:官方数据亮眼,社区测试显不足之处

引言 2025年4月&#xff0c;Meta正式发布了全新的Llama 4系列模型&#xff0c;这标志着Llama生态系统进入了一个全新的时代。Llama 4不仅是Meta首个原生多模态模型&#xff0c;还采用了混合专家(MoE)架构&#xff0c;并提供了前所未有的上下文长度支持。本文将详细介绍Llama 4…

【C++】函数直接返回bool值和返回bool变量差异

函数直接返回bool值和返回bool变量差异 背景 在工作中遇到一个比较诡异的问题&#xff0c;场景是给业务方提供的SDK有一个获取状态的函数GetStatus&#xff0c;函数的返回值类型是bool&#xff0c;在测试过程中发现&#xff0c;SDK返回的是false&#xff0c;但是业务方拿到的…

第1节:计算机视觉发展简史

计算机视觉与图像分类概述&#xff1a;计算机视觉发展简史 计算机视觉&#xff08;Computer Vision&#xff09;作为人工智能领域的重要分支&#xff0c;是一门研究如何使机器"看"的科学&#xff0c;更具体地说&#xff0c;是指用摄影机和计算机代替人眼对目标进行识…

英伟达Llama-3.1-Nemotron-Ultra-253B-v1语言模型论文快读:FFN Fusion

FFN Fusion: Rethinking Sequential Computation in Large Language Models 代表模型&#xff1a;Llama-3.1-Nemotron-Ultra-253B-v1 1. 摘要 本文介绍了一种名为 FFN Fusion 的架构优化技术&#xff0c;旨在通过识别和利用自然并行化机会来减少大型语言模型&#xff08;LLM…

云曦月末断网考核复现

Web 先看一个BUUCTF中的文件一个上传题 [BUUCTF] 2020新生赛 Upload 打开后是一个文件上传页面 随便上传一个txt一句话木马后出现js弹窗&#xff0c;提示只能上传图片格式文件 说明有前端验证。我的做法是把一句话改为.jpg格式&#xff0c; 然后上传 访问发现虽然上传成功了…

Flutter常用组件实践

Flutter常用组件实践 1、MaterialApp 和 Center(组件居中)2、Scaffold3、Container(容器)4、BoxDecoration(装饰器)5、Column(纵向布局)及Icon(图标)6、Column/Row(横向/横向布局)+CloseButton/BackButton/IconButton(简单按钮)7、Expanded和Flexible8、Stack和Po…

0.机器学习基础

0.人工智能概述&#xff1a; &#xff08;1&#xff09;必备三要素&#xff1a; 数据算法计算力 CPU、GPU、TPUGPU和CPU对比&#xff1a; GPU主要适合计算密集型任务&#xff1b;CPU主要适合I/O密集型任务&#xff1b; 【笔试问题】什么类型程序适合在GPU上运行&#xff1…

系统与网络安全------网络通信原理(4)

资料整理于网络资料、书本资料、AI&#xff0c;仅供个人学习参考。 网络层解析 IP 网络层概述 位于OSI模型第三层作用 定义网络设备的逻辑地址&#xff0c;俗称网络层地址&#xff08;如IP地址&#xff09; 在不同的网段之间选择最佳数据转发路径 协议 IP协议 IP数据包…

Java基础 4.12

1.方法的重载&#xff08;OverLoad&#xff09; 基本介绍 Java中允许同一个类&#xff0c;多个同名方法的存在&#xff0c;但要求形参列表不一致&#xff01; 如 System.out.println(); out是PrintStream类型 重载的好处 减轻了起名的麻烦减轻了记名的麻烦 2.重载的快速入…

XILINX DDR3专题---(1)IP核时钟框架介绍

1.什么是Reference Clock&#xff0c;这个时钟一定是200MHz吗&#xff1f; 2.为什么APP_DATA是128bit&#xff0c;怎么算出来的&#xff1f; 3.APP &#xff1a;MEM的比值一定是1:4吗&#xff1f; 4.NO BUFFER是什么意思&#xff1f; 5.什么情况下Reference Clock的时钟源可…

clickhouse注入手法总结

clickhouse 遇到一题clickhouse注入相关的&#xff0c;没有见过&#xff0c;于是来学习clickhouse的使用&#xff0c;并总结相关注入手法。 环境搭建 直接在docker运行 docker pull clickhouse/clickhouse-server docker run -d --name some-clickhouse-server --ulimit n…

React 组件样式

在这里插入图片描述 分为行内和css文件控制 行内 通过CSS中类名文件控制