一文搞懂 useDark:Vue 项目中实现深色模式的正确姿势

一文搞懂 useDark:Vue 项目中实现深色模式的正确姿势

效果

话不多说,先上效果

高亮效果

暗黑效果

前言

在近期的项目开发中,遇到了一个常见的用户体验需求:实现界面的高亮(亮色)与暗黑主题切换功能。用户通过点击按钮即可自由切换主题,并且系统需记住用户的偏好------将当前主题状态持久化至本地缓存,确保页面刷新或下次访问时仍保持最后一次选择的主题样式。 当前项目基于Vue 3 + Vite 构建,并使用了 Arco Design 作为 UI 组件库。在调研了多种实现方案后,结合社区优秀实践与相关技术文档,我最终选择了使用 VueUse 提供的响应式 API useDark,配合 SCSS 变量进行主题管理。该方案不仅简洁高效,而且具备良好的可维护性和扩展性,完美契合现代前端开发的需求。

思路

通过useDark实现,useDark() 的核心机制就是通过在根元素(通常是<html> <body>)上添加或移除一个特定的 CSS 类名(默认是 .dark) 来实现主题切换。

useDark() 的工作原理

  1. 状态管理:useDark() 内部会创建一个响应式的布尔值isDark,用于表示当前是否处于暗黑模式。
  2. DOM 操作:当 isDark 的值发生变化时(例如,用户点击了切换按钮),useDark() 会自动:

如果 isDarktrue,则在你指定的目标元素(默认是 document.documentElement,即 <html> 标签)上添加 .dark 类。 如果 isDarkfalse,则从该元素上移除 .dark 类。

  1. CSS 样式响应:你编写的 CSS/SCSS 样式会利用这个 .dark 类的存在与否来应用不同的主题样式。

暗黑主题样式应为class*="dark"

实现步骤

第一步、安装@vueuse/core

查看你们当前项目node_modeles是否包含@vueuse,如果没有可进行CDN引入和npm安装,我更倾向于使用npm安装方式安装。

  • npm安装

    bash 复制代码
    npm i @vueuse/core
  • CDN引入

    bash 复制代码
    <script src="https://unpkg.com/@vueuse/shared"></script>
    <script src="https://unpkg.com/@vueuse/core"></script>

第三步、pinia状态管理及localStorage存储

  1. stores中创建dark.ts文件,用于主题状态管理,后期可以添加其他主题。

  2. 编辑@/stores/dark.ts主题状态管理文件,因为你的页面不止有你自己的暗黑主题,还会有arcoDesign的主题,arcoDesign提供了切换主题方法,使用即可。

    typescript 复制代码
    import { defineStore } from 'pinia'
    import { Local } from '@/utils/storage'
    import { nextTick } from 'vue'
    import type { layoutConfigState } from '@/types/pinid'
    export const useLayoutConfigStore = defineStore('layoutConfig', {
      state: (): layoutConfigState => {
        return {
          isDark: false, // 黑暗模式
        }
      },
      getters: {
    
      },
      actions: {    // 更新状态
        updateState(state: layoutConfigState) {
          // 将传递的值更新到state状态中
          this.$patch(state)
          console.log("我被执行了", state)
        }
      }
    })
    nextTick(() => {
      const layoutConfig = useLayoutConfigStore()
      console.log("我被执行了", layoutConfig)
      // 监听状态变化,将状态持久化 
      layoutConfig.$subscribe((mutation, state) => {
        // 保存到浏览器的localStorage中
        if (state.isDark) {
          document.body.setAttribute('arco-theme', 'dark')
        } else {
          document.body.removeAttribute('arco-theme')
        }
        Local.set('layoutConfig', state)
      })
    })
  3. 编写utils/storage.ts缓存管理文件。

    typescript 复制代码
    /**
     * window.localStorage 浏览器永久缓存
     * @method set 设置永久缓存
     * @method get 获取永久缓存
     * @method remove 移除永久缓存
     * @method clear 移除全部永久缓存
     */
    export const Local = {
    	// 设置永久缓存
    	set(key: string, val: any) {
    		window.localStorage.setItem(key, JSON.stringify(val || ''));
    	},
    	// 获取永久缓存
    	get(key: string) {
    		let json: any = window.localStorage.getItem(key);
    		return JSON.parse(json);
    	},
    	// 移除永久缓存
    	remove(key: string) {
    		window.localStorage.removeItem(key);
    	},
    	// 移除全部永久缓存
    	clear() {
    		window.localStorage.clear();
    	},
    };
    
    /**
     * window.sessionStorage 浏览器临时缓存
     * @method set 设置临时缓存
     * @method get 获取临时缓存
     * @method remove 移除临时缓存
     * @method clear 移除全部临时缓存
     */
    export const Session = {
    	// 设置临时缓存
    	set(key: string, val: any) {
    		// val为undefined保存时,当get时parse转成json对象会失败,加上 || ''解决这个
    		window.sessionStorage.setItem(key, JSON.stringify(val || ''));
    	},
    	// 获取临时缓存
    	get(key: string) {
    		let json: any = window.sessionStorage.getItem(key);
    		// parse 无法对undefined 和 ''进行解析的
    		return JSON.parse(json);
    	},
    	// 移除临时缓存
    	remove(key: string) {
    		window.sessionStorage.removeItem(key);
    	},
    	// 移除全部临时缓存
    	clear() {
    		window.sessionStorage.clear();
    	},
    };

第二步、通过开关控制pinia及状态管理

  1. 引入useDark 及状态管理文件

    typescript 复制代码
    import { useDark } from "@vueuse/core";
    import { useLayoutConfigStore } from "../../stores/dark";
    const layoutConfig = useLayoutConfigStore();
  2. 添加主题开关

    typescript 复制代码
    <a-switch
    	v-model="isDark"
    	  @change="changeDark"
    	  inline-prompt
    	  active-icon="ele-Moon"
    	  inactive-icon="ele-Sunny"
    >
      <template #checked-icon>
        <icon-moon-fill />
      </template>
      <template #unchecked-icon> <icon-sun-fill /> </template
    ></a-switch>
  3. 监听isDark变化,设置对应主题

    typescript 复制代码
    // 会自动监听isDark变化,来设置对应的主题模式
    const isDark = useDark({
      valueDark: "dark", // 暗黑模式,在 html元素的class属性值
      valueLight: "", // 高亮模式,在 html元素的class属性值
      initialValue: "dark", // 初始模式: dark|auto ( 高亮模式)
    });
  4. 切换主题逻辑实现

    typescript 复制代码
    // 切换暗黑模式,更新状态值
    function changeDark(isDark) {
      console.log("isDark:", isDark);
      layoutConfig.isDark = isDark;
    }

第四步、创建高亮及暗黑.scss文件

  1. style中创建一个app.scssdark.scss文件,app.scss大部分样式用来高亮主题,dark.scss主要用来暗黑样式。

  2. 创建@styles/index.scss文件,用于引用app.scssdark.scss样式,注意dark.scss的权重要比app.scss权重要高,引用顺序相对于靠后。

  3. main.ts引入@/styles/index.scss样式文件

    css 复制代码
    import '@/styles/index.scss';
  4. 编写app.scss样式文件,大多为样式变量,以下是部分样式代码:

css 复制代码
	/* 初始化样式
	------------------------------- */
	:root {
	  /* 模态框背景颜色 */
	  --wyk-modal-bg-color: rgba(23, 23, 26, 0.6);
	  /*菜单样式开始*/
	  --wyk-menu-font-color: #000; // 菜单字体颜色
	  --wyk-menu-bg-color: #fff; // 菜单背景颜色
	  --wyk-menu-divider-color: #ededed;
	  /*菜单样式结束*/
	  /*白天黑夜字体颜色与背景颜色开始*/
	  --wyk-font-color: #000;
	  --wyk-bg-color: #fff;
	  --wyk-grey-font-color: #333;
	  --wyk-grey-border-color: #d5d5d5;
	  --wyk-btn-color: #fff;
	  --wyk-btn-boder-color: #325ab5;
	
	  /*白天黑夜字体颜色与背景颜色结束*/
	
	  /* home 页面黑夜白天主题添加开始*/
	  --wyk-home-item-content: #f5f8fe;
	  /* home 页面黑夜白天主题添加结束*/
	
	  /* dataset 数据集页面白天黑夜主题添加 */
	  --wyk-dataset-bg-colod: #f5f5f5;
	  --wyk-dataset-title-box: #fff;
	
	  /* task management 任务管理背景色*/
	  --wyk-task-box-bgcolor: #fff;
	  --wyk-task-bgcolor: #f5f5f5;
	  --wyk-task-title-color: #000;
	  --wyk-task-tab-hover-color: #f5f8fe;
	
	  /* 日志管理样式*/
	  --wyk-log-border-color: #dcdfe6;
	
	  /*模型管理样式*/
	  --wyk-model-box-bgcolor: #fff;
	  --wyk-model-border-color: #dcdfe6;
	
	  /*标签管理样式*/
	  --wyk-label-box-bgcolor: #fff;
	  --wyk-label-border-color: #dcdfe6;
	
	  /* 用户角色样式*/
	  --wyk-user-box-bgcolor: #fff;
	
	  /* 悬浮球弹窗样式 */
	  --wyk-float-ball-pop-bgcolor: #fff;
	  --wyk-float-ball-pop-box-shadow: #f2f6fd;
	  --wyk-float-ball-progress-bgcolor: #f2f6fd;
	
	  --wyk-color-theme: #325ab5;
	  --wyk-color-white: #fff;
	  // --wyk-color-black: #2b2f38;
	  --wyk-color-primary: #325ab5;
	  --wyk-border-color: #f1f2f3;
	
	  --wyk-color-hover: #3c3c3c;
	  --wyk-color-hover-rgba: rgba(0, 0, 0, 0.05);
	
	  // 左侧菜单
	  --wyk-bg-menuMainColor: var(--wyk-menu-bg-color) !important;
	  --wyk-bg-menuActiveColor: var(--wyk-color-black) !important;
	  --wyk-bg-menuHoverColor: var(--wyk-color-black) !important;
	
	  --wyk-text-menuMainColor: var(--wyk-color-black) !important;
	  --wyk-text-menuActiveColor: var(--wyk-color-white) !important;
	  --wyk-text-menuHoverColor: var(--wyk-color-white) !important;
	
	  // 头部导航
	  --wyk-bg-headerBarColor: var(--wyk-color-white) !important;
	
	  // 头部右侧图标光标浮动
	  --wyk-color-user-hover: var(--wyk-color-hover-rgba) !important;
	
	  // 边框
	  --wyk-border-color-light: var(--wyk-border-color) !important;
	}
	.
	.
	.
  1. 编写dark.scss样式文件,大多为样式变量,以下是部分暗黑主题样式代码:
css 复制代码
/* 暗黑样式
--------------------------- */
html[class*="dark"] {
  /* 模态框背景颜色 */
  --wyk-modal-bg-color: rgba(23, 23, 26, 0.8);
  /*菜单样式开始*/
  --wyk-menu-font-color: #fff; // 菜单字体颜色
  --wyk-menu-bg-color: #333333;
  --wyk-menu-divider-color: #fff;
  /*菜单样式结束*/
  /*白天黑夜字体颜色与背景颜色开始*/
  --wyk-font-color: #c0c0c0;
  --wyk-bg-color: #333333;
  --wyk-scrollbar-color: #333333;
  --wyk-grey-font-color: #c0c0c0;
  --wyk-grey-border-color: #c0c0c0;
  --wyk-btn-color: #404040;
  --wyk-btn-boder-color: #404040;
  /*白天黑夜字体颜色与背景颜色结束*/
  /* home 页面黑夜白天主题添加开始*/
  --wyk-home-item-content: #333333;
  /* home 页面黑夜白天主题添加结束*/
  /* dataset 数据集页面白天黑夜主题添加 */
  --wyk-dataset-bg-colod: #333333;
  --wyk-dataset-title-box: #333333;
  /* task management 任务管理背景色*/
  --wyk-task-box-bgcolor: #333333;
  --wyk-task-bgcolor: #333333;
  --wyk-task-title-color: #c0c0c0;
  --wyk-task-tab-hover-color: rgba(245, 248, 254, 0.1);
  /* 日志 样式*/
  --wyk-log-border-color: #333333;
  /*模型管理样式*/
  --wyk-model-box-bgcolor: #333;
  --wyk-model-border-color: #333;
  /*标签管理样式*/
  --wyk-label-box-bgcolor: #333;
  --wyk-label-border-color: #333;
  /* 用户样式*/
  --wyk-user-box-bgcolor: #333;
  /* 悬浮球弹窗样式 */
  --wyk-float-ball-pop-bgcolor: #333;
  --wyk-float-ball-pop-box-shadow: #333;
  --wyk-float-ball-progress-bgcolor: #423f3f;
  --wyk-color-theme: #325ab5;
  --wyk-color-white: #fff;
  // --wyk-color-black: #2b2f38;
  --wyk-color-primary: #325ab5;
  --wyk-border-color: #f1f2f3;
  --wyk-color-hover: #3c3c3c;
  --wyk-color-hover-rgba: rgba(0, 0, 0, 0.05);
  // 菜单
  --wyk-bg-menuMainColor: var(--wyk-color-black) !important;
  --wyk-bg-menuActiveColor: var(--wyk-color-primary) !important;
  --wyk-bg-menuHoverColor: #2e436e !important;
  --wyk-text-menuMainColor: var(--wyk-color-white) !important;
  --wyk-text-menuActiveColor: var(--wyk-color-white) !important;
  --wyk-text-menuHoverColor: var(--wyk-color-white) !important;

  --wyk-bg-headerBarColor: var(--wyk-color-black) !important;
  // 头部右侧图标光标浮
  --wyk-color-user-hover: var(--wyk-color-hover-rgba) !important;
  // 边框
  --wyk-border-color-light: var(--wyk-border-color) !important;
  /* wangeditor 富文本编辑器 - css vars
  --------------------------- */
  --w-e-toolbar-bg-color: var(--el-bg-color) !important;
  --w-e-toolbar-color: var(--el-text-color-primary) !important;
  // // 工具栏浮动显示
  --w-e-toolbar-active-color: var(--el-text-color-primary) !important;
  --w-e-toolbar-active-bg-color: var(--el-color-primary-light-9) !important;
  --w-e-toolbar-border-color: var(--wyk-border-color) !important;
  // // 内容区域
  --w-e-textarea-bg-color: var(--el-bg-color) !important;
}

第五步、在页面中使用scssvar()方法使用样式变量

css 复制代码
<style lang="scss" scoped></style>

第六步、app.vue页面进行主题初始化

因为主题配置都放在了pinia中和localStorage中,需要在app.vueonMounted中拿到本地缓存的layoutConfig主题信息更新到pinia中,而且还需要将arcoDesign的主题设置为暗黑主题,arcoDesign官网有主题切换代码,直接拷贝即可。

typescript 复制代码
<script setup lang="ts"></script>
<template>
  <!-- 不要少了`class="h100"` 高度100%,不然布局的高度无法占满 -->
  <div class="h100">
    <!-- 路由组件渲染出品 -->
    <router-view></router-view>
  </div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { Local } from "../src/utils/storage";
import { useLayoutConfigStore } from "../src/stores/dark";

const layoutConfig = useLayoutConfigStore();

onMounted(() => {
  // 获取localStorage中的布局配置数据,
  const layoutConfigState = Local.get("layoutConfig");
  console.log("layoutConfigState-----------", layoutConfigState);
  if (layoutConfigState === null) {
    layoutConfig.isDark = false;
    document.body.removeAttribute("arco-theme");
  } else {
    if (layoutConfigState.isDark) {
      document.body.setAttribute("arco-theme", "dark");
    } else {
      layoutConfig.isDark = false;
      document.body.removeAttribute("arco-theme");
    }
  }

  // 更新到pinia状态中
  if (layoutConfigState) layoutConfig.updateState(layoutConfigState);
});
</script>
<style scoped></style>

第七步、查看实现效果

结语

高亮暗黑主题切换的功能到这里就结束了,如果对你有帮助别忘一键三联哦~

相关推荐
狂炫一碗大米饭3 分钟前
Vue 3 的最佳开源分页库
前端·程序员·设计
你听得到1120 分钟前
告别重复造轮子!我从 0 到 1 封装一个搞定全场景的弹窗库!
前端·flutter·性能优化
Ali酱26 分钟前
2周斩获远程offer!我的高效求职秘诀全公开
前端·后端·面试
Cyanto1 小时前
Vue浅学
前端·javascript·vue.js
一只小风华~1 小时前
CSS aspect-ratio 属性
前端·css
Silver〄line1 小时前
以鼠标位置为中心进行滚动缩放
前端
LaiYoung_1 小时前
深入解析 single-spa 微前端框架核心原理
前端·javascript·面试
Danny_FD2 小时前
Vue2 + Node.js 快速实现带心跳检测与自动重连的 WebSocket 案例
前端
uhakadotcom2 小时前
将next.js的分享到twitter.com之中时,如何更新分享卡片上的图片?
前端·javascript·面试