最近要做主题换肤的功能,项目用的 UI 框架是 ViewDesign ,只支持定制主题,不支持动态主题。查了好多资料,比如:用 less 变量更换、预设几种主题的 CSS 的文件,再动态切换 link 标签的引入资源路径等等。这些方案的应用场景是,对项目中完全自主开发的模块的颜色进行切换。也即,你自己开发了一个导航栏(Menu)和顶部(Header),这俩你是完全控制的,用上述提到的方案是完全可行的。而 UI 框架是第三方的,用 less 变量替换,也仅限于在编译前,编译后它就是 CSS 格式了,再想用 less 变量是不可行的。而预设几种主题 CSS 文件,对于已经编译后的组件库,外部引入组件库的 CSS 不会对内部有任何影响(也有可能是优先级不够 🤔)。ViewDesign 官方提供了定制主题的方式,可以对一些 less 变量进行重新覆盖,这些变量,在编译后,肯定就转成 CSS 格式了。比如 @color1 编译后就是 red@color2 编译后就是 blue。它们也是有映射关系的,而且这种关系是一对一的。那么,如果把编译后的 CSS 中的颜色值,都改为 CSS var() 的格式,然后再外部动态设置这些 CSS 变量,是不是就能实现动态主题了?好,开干!

1. 安装主题生成工具

npm install iview-theme -g

这个工具官方有介绍,版本有点老,不过只需要用它打包一下,够用了。

2. 获取对应版本的样式文件

官方虽然说这样可以下载对应版本的,但是我试了,并不可行,可能不支持高版本。如果想要下载指定版本的,可以先下载指定版本的 ViewDesign (我用的是 v4.7.0 ):

npm install view-design@4.7.0

然后找到 node_modules/view-design/src/styles 目录,这个文件夹下的,就是对应版本的样式文件,复制出来一份就行。

3. 主题色相关颜色转换

下载后的样式文件里,有一个 custom.less 文件,这里罗列了所有可配置的颜色,我们只需要在意所有与主题色有关的就行。如下(后面的注释编号,代表当前的颜色种类,也即共有 10 种颜色与主题色有关):

// Color
@primary-color: #2d8cf0; // 1
@processing-color: @primary-color; // 1
@link-color: #2d8cf0; // 1
@link-hover-color: tint(@link-color, 20%); // 2
@link-active-color: shade(@link-color, 5%); // 3
@selected-color: fade(@primary-color, 90%); // 4

// Button
@btn-group-border: shade(@primary-color, 5%); // 3
@btn-primary-bg: @primary-color; // 1

// Input
@input-hover-border-color: @primary-color; // 1
@input-focus-border-color: @primary-color; // 1

// Slider
@slider-color: tint(@primary-color, 20%); // 2

// 特殊的,有些需要额外替换
// 表单的聚焦外阴影
.ivu-input:focus {
  box-shadow: 0 0 0 2px fade(@primary-color, 20%); // 5
}

// 树形组件悬浮和选中时的背景色
.ivu-tree-title:hover {
  background-color: tint(@primary-color, 90%); // 6
}
.ivu-tree-title-selected,
.ivu-tree-title-selected:hover {
  background-color: tint(@primary-color, 80%); // 7
}

// 导航菜单激活时的背景颜色
.ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu) {
  background: #f0faff; // 8 => tint(@primary-color, 95%)
}

// 不是从主题色中延申而来的颜色,用主题色延申色替换
@table-td-hover-bg: #ebf7ff; // 9 => tint(@primary-color, 90%)
@table-td-highlight-bg: #ebf7ff; // 9 => tint(@primary-color, 90%)
@date-picker-cell-hover-bg: #e1f0fe; // 10 => tint(@primary-color, 85%)

你可能注意到了,有些完全和主题色八竿子打不着,确实如此。把所有组件都测试了一遍后,发现有些组件使用的颜色是固定的颜色值,要想和主题色相关联,只能把这个颜色替换成由主题色延申的方式,颜色 8、9 和 10 就是如此,用主题色生成了一个差不多类似的颜色,差别微乎其微,还可以接受。

接着把其中由 less 函数生成的,都转为最后的实际颜色,这一步是为了方便后续的全局替换。

在线编译 lesshttps://www.dute.org/less-to-css

转换后如下:

// Color
@primary-color: #2d8cf0; // 1
@processing-color: #2d8cf0; // 1
@link-color: #2d8cf0; // 1
@link-hover-color: #57a3f3; // 2
@link-active-color: #2b85e4; // 3
@selected-color: rgba(45, 140, 240, 0.9); // 4

// Button
@btn-group-border: #2b85e4; // 3
@btn-primary-bg: #2d8cf0; // 1

// Input
@input-hover-border-color: #2d8cf0; // 1
@input-focus-border-color: #2d8cf0; // 1

// Slider
@slider-color: #57a3f3; // 2

// 特殊的,有些需要额外替换
// 表单的聚焦外阴影
.ivu-input:focus {
  box-shadow: 0 0 0 2px rgba(45, 140, 240, 0.2); // 5
}

// 树形组件悬浮和选中时的背景色
.ivu-tree-title:hover {
  background-color: #eaf4fe; // 6 tint(@primary-color, 90%)
}
.ivu-tree-title-selected,
.ivu-tree-title-selected:hover {
  background-color: #d5e8fc; // 7 tint(@primary-color, 80%)
}

// 导航菜单激活时的背景颜色
.ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu) {
  background: #f0faff; // 8 => tint(@primary-color, 95%)
}

// 不是从主题色中延申而来的颜色,用主题色延申色替换
@table-td-hover-bg: #ebf7ff; // 9 => tint(@primary-color, 90%)
@table-td-highlight-bg: #ebf7ff; // 9 => tint(@primary-color, 90%)
@date-picker-cell-hover-bg: #e1f0fe; // 10 => tint(@primary-color, 85%)

4. 修改编译后的 CSS 文件

执行打包命令:

iview-theme build -o dist/

如果打包报如下错误,把当前 Node 版本换成 v11 版本即可,亲测有效(PS:我用的 v11.15.0 )。

打开 dist/iview.css 文件,将上述的 10 种颜色,都转为以 CSS 变量的方式,同时将预留值设为原颜色值。转换规则如下:

  1. #2d8cf0:替换为 var(--ivu-primary-color, #2d8cf0),总计 113 个;
  2. #57a3f3:替换为 var(--ivu-link-hover-color, #57a3f3),总计 46 个;
  3. #2b85e4:替换为 var(--ivu-link-active-color-color, #2b85e4),总计 22 个;
  4. rgba(45,140,240,.9):替换为 var(--ivu-selected-color, rgba(45,140,240,.9)),总计 2 个。
  5. rgba(45,140,240,.2):替换为 var(--ivu-input-shadow-color, rgba(45,140,240,.2)),总计 25 个。
  6. #eaf4fe:替换为 var(--ivu-tree-title-hover-color, #eaf4fe),总计 1 个。
  7. #d5e8fc:替换为 var(--ivu-tree-title-selected-color, #d5e8fc),总计 4 个。
  8. #f0faff:替换为 var(--ivu-menu-active-color, #f0faff),总计 5 个。
  9. #ebf7ff:替换为 var(--ivu-table-hover-bg-color, #ebf7ff),总计 3 个。
  10. #e1f0fe:替换为 var(--ivu-date-picker-hover-bg-color, #e1f0fe),总计 2 个。

5. 通过主题色生成各种延申色

延申色都是由 less 内置函数生成的,最好的办法也就是还用 less 提供的函数去生成,经历一番探索,终于找到了 less 中函数的位置(藏的可太深了/(ㄒ o ㄒ)/~~)。最终的转换方法如下:

import less from "less"

const Color = less.color
const { fade, shade, tint } = less.functions.functionRegistry._data

function toRgb(color) {
  const { alpha, rgb } = color
  const [r, g, b] = rgb.map(v => parseInt(v, 10))
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

/**
 * 生成主题色
 * @param {string} themeColor 十六进制的颜色值
 * @returns
 */
export default function genThemeColor(themeColor) {
  const color = new Color(themeColor.slice(1))

  const linkHoverColor = tint(color, { value: 20 })
  const linkActiveColor = shade(color, { value: 5 })
  const selectedColor = fade(color, { value: 90 })
  const inputShadowColor = fade(color, { value: 20 })
  const treeTitleHoverColor = tint(color, { value: 90 })
  const treeTitleSelectedColor = tint(color, { value: 80 })
  const menuActiveColor = tint(color, { value: 95 })
  const tableHoverBgColor = tint(color, { value: 90 })
  const datePickerHoverBgColor = tint(color, { value: 85 })

  const colors = {
    "--ivu-primary-color": themeColor,
    "--ivu-link-hover-color": toRgb(linkHoverColor),
    "--ivu-link-active-color-color": toRgb(linkActiveColor),
    "--ivu-selected-color": toRgb(selectedColor),
    "--ivu-input-shadow-color": toRgb(inputShadowColor),
    "--ivu-tree-title-hover-color": toRgb(treeTitleHoverColor),
    "--ivu-tree-title-selected-color": toRgb(treeTitleSelectedColor),
    "--ivu-menu-active-color": toRgb(menuActiveColor),
    "--ivu-table-hover-bg-color": toRgb(tableHoverBgColor),
    "--ivu-date-picker-hover-bg-color": toRgb(datePickerHoverBgColor)
  }

  // 设置主题色 CSS 变量
  Object.keys(colors).forEach(key => {
    document.body.style.setProperty(key, colors[key])
  })
}

也即,只需要提供一个主题色,其他延申色全由函数内部自动生成,并且把 CSS 变量设置在 body 上。看下效果如何:

😁,效果不错吧。最后,如果你也用的 v4.7.0 ,我把对应的样式文件和主题色生成 JS 都放在了 这里 ,可以直接下载使用。如果你用的其他版本,可以参考前文流程,去下载对应版本的样式文件,然后将各个颜色统一替换为 CSS 变量的格式。不同版本,基础主题的颜色值可能有差异,建议使用前确认一下。