YGG-CLI-9-主题切换的设计与开发页面

一个文笔一般,想到哪是哪的唯心论前端小白。

🧠 - 简介

主题切换的方案,网上有好多种,而我要实现这一种:

其实是模仿了 element-plus 官网的主题切换效果,它那里只是实现了亮色和暗色的切换,我则是在 element-plus 明亮切换的基础上,增加了 主色 的切换。

👁️ - 分析

主题切换站在产品的角度其实分为几个档:

  1. 明暗切换:我的世界里只有 0 和 1,非黑即白,其实主要是修改的反而是中性色的色值,例如:背景色,文本颜色,边框颜色等。
  2. 主色切换:主色切换和明暗切换,则是刚好相反,是在切换主色,也是我这在这个模板项目中使用的方案。
  3. 全站定制主题色:把所有的颜色提取出来进行归类,都使用变量控制,然后将这些变量进行设计定制。工作量很大。

另外总结一下一个网站上常用的几类颜色:

  • 主色:网站的主要颜色,例如:京东的红色,支付宝的蓝色,美团的黄色等等。
  • 辅助色:辅助色主要是和主色交相辉映的,为了衬托主色。
  • 点缀色(功能色):在element-plus 中的具体体现就是:警告、错误、成功这些提示颜色。
  • 中性色:背景、边框、文本颜色。

🫀 - 拆解

通过效果图可以看出来,这个主题切换分为如下几个部分:

  1. 明亮和暗黑切换的时候会出现一个动画效果,暗黑向亮白转换时,圆形由小到大,亮白向暗黑转换则相反。
  2. 都是明亮或者都是暗黑时,则不出现动画。
  3. 亮白和暗黑后面的颜色一样时,主色没有变化。
  4. 当下次进入系统时,要记录上次的主色,并进行主色切换。

所以开发起来就很顺利了,主要是实现这个动画效果,然后实现主色的切换。

💪 - 落实

动画效果讲解

动画切换是不是一下子想不起来怎么做?我也是查了好多资料才查到的。

它主要是用到了一个不怎么常用的api:document.startViewTransition,看过兼容性才知道,兼容性并没有想象中的那么优秀,所以还要做一下兼容,如果浏览器不支持,就不进行动画了。

所以就出现了下面这段逻辑:

ts 复制代码
// 主题切换动画
const animationTheme = (x: Ref<number>, y: Ref<number>, isToggle: boolean, color: string) => {
    // 开始一次视图过渡:
    const transition = document.startViewTransition(() => {
      changePrimaryColor(colorMap[color])
      isToggle && toggleDark()
    });

    transition.ready.then(() => {
      //计算按钮到最远点的距离用作裁剪圆形的半径
      const endRadius = Math.hypot(
        Math.max(x.value, innerWidth - x.value),
        Math.max(y.value, innerHeight - y.value)
      );
      const clipPath = [
        `circle(0px at ${x.value}px ${y.value}px)`,
        `circle(${endRadius}px at ${x.value}px ${y.value}px)`,
      ];
      //开始动画
      document.documentElement.animate(
        {
          clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
        },
        {
          duration: 400,
          easing: "ease-in",
          pseudoElement: isDark.value
            ? "::view-transition-old(root)"
            : "::view-transition-new(root)",
        }
      );
    });
  }

// 操作主题切换
const handleChangeTheme = useThrottleFn((v: string) => {

    if (v === localStorage.getItem('theme')) {
      return
    }
    const [mode, color] = v.split('-')
    const isToggle = (!isDark.value && mode === 'dark') || (isDark.value && mode === 'light')

    theme.value = v
    localStorage.setItem('theme', v)

    //在不支持的浏览器里不做动画
    if (document.startViewTransition) {
      if (isToggle) {
        animationTheme(x, y, isToggle, color)
      } else {
        changePrimaryColor(colorMap[color])
      }
    } else {
      changePrimaryColor(colorMap[color])
      isToggle && toggleDark()
    }
  }, 400)

上面还是用了节流函数进行包裹操作方法,避免动画进行到一半就被打断的尴尬局面 ~ ~ ~

startViewTransition[MDN] 地址,可以自己学习一下,我也正在学习这些不怎么常用的API。

主色生成器

通过看 element-plus 主题源码不难看出,主色和功能色其实都是一组由浅到深的渐变色,所以切换主色并不只是切换一个色值,而是切换一组色值。

所以就实现了一个色阶生成器:

ts 复制代码
/**
 * 创建两种颜色之间的混合色。
 * @param color1 第一种颜色值。
 * @param color2 第二种颜色值。
 * @param ratio 混合比例(0 到 1 之间)。
 * @returns 返回混合后的颜色值。
 */
function mix(color1: string, color2: string, ratio: number): string {
  // 解析第一种颜色的 RGB 值
  const r1 = parseInt(color1.substring(1, 3), 16);
  const g1 = parseInt(color1.substring(3, 5), 16);
  const b1 = parseInt(color1.substring(5, 7), 16);

  // 解析第二种颜色的 RGB 值
  const r2 = parseInt(color2.substring(1, 3), 16);
  const g2 = parseInt(color2.substring(3, 5), 16);
  const b2 = parseInt(color2.substring(5, 7), 16);

  // 计算混合后的 RGB 值
  const mixedR = Math.round(r1 * (1 - ratio) + r2 * ratio);
  const mixedG = Math.round(g1 * (1 - ratio) + g2 * ratio);
  const mixedB = Math.round(b1 * (1 - ratio) + b2 * ratio);

  // 构建混合后的颜色值
  const mixedColor = `#${mixedR.toString(16)}${mixedG.toString(16)}${mixedB.toString(16)}`;

  return mixedColor;
}

然后进行变量替换

ts 复制代码
/**
 * 改变网页中特定元素的主题颜色。
 * @param e 选定的颜色值。
 */
export function changePrimaryColor(e: string): void {
  const pre = "--el-color-primary";
  const mixWhite = "#ffffff";
  const mixBlack = "#000000";
  const el = document.documentElement;

  // 设置主要颜色
  el.style.setProperty(pre, e);

  for (let i = 1; i < 10; i += 1) {
    // 设置不同明度的颜色值
    el.style.setProperty(`${pre}-light-${i}`, mix(e, mixWhite, i * 0.1));
  }

  // 设置较深的颜色值
  el.style.setProperty("--el-color-primary-dark", mix(e, mixBlack, 0.1));
}

这样就实现了主色的切换。

🛀 - 总结

上面四个核心方法就是这个主题切换的核心功能了,最后为了凑字数,粘上这个抽屉的源码:

vue 复制代码
<template>
  <Drawer
    ref="themeDrawer"
    :show-cancel="false"
    :confirm-text="'关闭'"
    @confirm="handleConfirm"
  >
    <div>
      <ul>
        <li
          v-for="item of themeList "
          :key="item.value"
          :class="{ 'theme-current': theme == item.value }"
          class="theme-item"
          @click="handleChangeTheme(item.value)"
        >
          <p>
            <span
              v-for="i in 4 "
              :key="i"
              :style="`background: ${['red', 'yellow', 'blue', 'green'][i - 1]}; height: 24px; width: 36px; display: inline-block;`"
            />
          </p>
          <p> {{ item.label }}</p>
          <span
            v-if="theme === item.value"
            class="current-jiaobiao"
          ><i class="iconfont icon-jiaobiao" /></span>
        </li>
      </ul>
    </div>
  </Drawer>
</template>

<script setup lang="ts">
  /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
   * @Name: CommonTheme
   * @Author: Zhang Ziyi
   * @Email: 15227974559@163.com
   * @Date: 2024-03-19 14:28
   * @Introduce: --
   * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
  import { onMounted, ref, Ref } from 'vue'
  import {
    useLocalStorage, useDark, useMouseInElement, useThrottleFn
  } from '@vueuse/core'
  import { useToggle } from '@vueuse/shared'
  import { changePrimaryColor } from '@/utils/themeColorGenerate'

  // Refs
  const themeDrawer = ref()

  // 数据
  const theme = ref<string>('')
  theme.value = useLocalStorage('theme', 'light-blue').value
  const themeList = ref([
    { label: '暗黑-红', value: 'dark-red' },
    { label: '亮白-红', value: 'light-red' },
    { label: '暗黑-金', value: 'dark-yellow' },
    { label: '亮白-金', value: 'light-yellow' },
    { label: '暗黑-蓝', value: 'dark-blue' },
    { label: '亮白-蓝', value: 'light-blue' },
    { label: '暗黑-绿', value: 'dark-green' },
    { label: '亮白-绿', value: 'light-green' },
  ])

  const colorMap: { [k: string]: string } = {
    red: '#FF0000',
    yellow: '#FFD700',
    blue: '#0000FF',
    green: '#00FF00'
  }

  onMounted(() => {
    console.log('🎡 > 主题色初始化完成,当前主题色为:', theme.value, '。');
    initTheme()
  })

  const initTheme = () => {
    const [mode, color] = theme.value.split('-');
    (!isDark.value && mode === 'dark') && toggleDark()
    changePrimaryColor(colorMap[color])
  }

  // 切换主题相关方法
  const { x, y } = useMouseInElement()
  const isDark = useDark()
  const toggleDark = useToggle(isDark)

  // 主题切换动画
  const animationTheme = (x: Ref<number>, y: Ref<number>, isToggle: boolean, color: string) => {
    // 开始一次视图过渡:
    const transition = document.startViewTransition(() => {
      changePrimaryColor(colorMap[color])
      isToggle && toggleDark()
    });

    transition.ready.then(() => {
      //计算按钮到最远点的距离用作裁剪圆形的半径
      const endRadius = Math.hypot(
        Math.max(x.value, innerWidth - x.value),
        Math.max(y.value, innerHeight - y.value)
      );
      const clipPath = [
        `circle(0px at ${x.value}px ${y.value}px)`,
        `circle(${endRadius}px at ${x.value}px ${y.value}px)`,
      ];
      //开始动画
      document.documentElement.animate(
        {
          clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
        },
        {
          duration: 400,
          easing: "ease-in",
          pseudoElement: isDark.value
            ? "::view-transition-old(root)"
            : "::view-transition-new(root)",
        }
      );
    });
  }

  const handleChangeTheme = useThrottleFn((v: string) => {

    if (v === localStorage.getItem('theme')) {
      return
    }
    const [mode, color] = v.split('-')
    const isToggle = (!isDark.value && mode === 'dark') || (isDark.value && mode === 'light')

    theme.value = v
    localStorage.setItem('theme', v)

    //在不支持的浏览器里不做动画
    if (document.startViewTransition) {
      if (isToggle) {
        animationTheme(x, y, isToggle, color)
      } else {
        changePrimaryColor(colorMap[color])
      }
    } else {
      changePrimaryColor(colorMap[color])
      isToggle && toggleDark()
    }
  }, 400)

  const handleCancel = (cb: () => void) => cb()
  const handleConfirm = (cb: () => void) => cb()

  // 对外暴露方法
  const open = () => {
    themeDrawer.value.open('主题切换')
  }
  defineExpose({
    open
  })
</script>

<style lang="scss" scoped>
.theme-item {
  cursor: pointer;
  position: relative;
  margin: 8px 0;
  border: 1px solid var(--el-border-color-light);

  .current-jiaobiao {
    position: absolute;
    bottom: 0;
    right: 0;
    display: block;
    color: var(--el-color-primary);
  }

  &.theme-current {
    background: var(--el-color-primary-light-5);
    border-color: var(--el-color-primary-light-5);
  }

  padding: 10px;

  &:hover {
    background: var(--el-color-primary-light-8);
  }
}
</style>

后续会把一些方法提出去,减轻这个组件的体积,方便方便维护。

系列文章:

  1. 脚手架开发
  2. 模板项目初始化
  3. 模板项目开发规范与设计思路
  4. layout设计与开发
  5. login 设计与开发
  6. CURD页面的设计与开发
  7. 监控页面的设计与开发
  8. 富文本编辑器的使用与页面设开发设计
  9. 主题切换的设计与开发并页面
  10. 水印切换的设计与开发
  11. 全屏与取消全屏
  12. 开发提效之一键生成模块(页面)
相关推荐
用户14567756103714 分钟前
亲测好用!简单实用的图片尺寸调整工具
前端
索西引擎15 分钟前
npm、yarn、pnpm
前端·npm·node.js
天生我材必有用_吴用1 小时前
Vue3 + VitePress 搭建组件库文档平台(结合 Element Plus 与 Arco Design Vue)—— 超详细图文教程
前端
liu****1 小时前
基于websocket的多用户网页五子棋(八)
服务器·前端·javascript·数据库·c++·websocket·个人开发
San301 小时前
深入理解 JavaScript 函数:从基础到高阶应用
前端·javascript·node.js
ttyyttemo1 小时前
Column,rememberScrollState,记住滚动位置
前端
芒果茶叶2 小时前
并行SSR,SSR并行加载
前端·javascript·架构
vortex52 小时前
解决 Kali 中 Firefox 下载语言包和插件速度慢的问题:配置国内镜像加速
前端·firefox·腾讯云
修仙的人2 小时前
Rust + WebAssembly 实战!别再听说,学会使用!
前端·rust
maxine2 小时前
JS Entry和 HTML Entry
前端