diff --git a/src/components/chart/MiniProgress.vue b/src/components/chart/MiniProgress.vue index ef9157a..c4d0388 100644 --- a/src/components/chart/MiniProgress.vue +++ b/src/components/chart/MiniProgress.vue @@ -25,7 +25,7 @@ export default { position: relative; width: 100%; .wrap { - background-color: #f5f5f5; + background-color: @bg-color; position: relative; } .progress { diff --git a/src/components/chart/RankingList.vue b/src/components/chart/RankingList.vue index 1ff989f..ec7b309 100644 --- a/src/components/chart/RankingList.vue +++ b/src/components/chart/RankingList.vue @@ -48,7 +48,7 @@ export default { } span.active { background-color: #314659 !important; - color: #fff !important; + color: @text-color-inverse !important; } span:last-child { float: right; diff --git a/src/components/setting/Setting.vue b/src/components/setting/Setting.vue index 8cd02d9..b97ef46 100644 --- a/src/components/setting/Setting.vue +++ b/src/components/setting/Setting.vue @@ -97,7 +97,7 @@ export default { return { animate: this.$store.state.setting.animate.name, direction: this.$store.state.setting.animate.direction, - colors: ['#f5222d', '#fa541c', '#fadb14', '#49aa19', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96'], + colors: ['#f5222d', '#fa541c', '#fadb14', '#3eaf7c', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96'], } }, computed: { diff --git a/src/config/default/theme.js b/src/config/default/theme.js index 21af6e2..3482248 100644 --- a/src/config/default/theme.js +++ b/src/config/default/theme.js @@ -1,3 +1,9 @@ +// 主题模式 +const mode = { + LIGHT: 'light', + DARK: 'dark', + NIGHT: 'night', +} // 亮色模式 const light = { 'layout-body-background': '#f0f2f5', @@ -61,4 +67,4 @@ const night = { 'btn-primary-color': '#141414', } -module.exports = {light, dark, night} +module.exports = {light, dark, night, mode} diff --git a/src/config/replacer/index.js b/src/config/replacer/index.js new file mode 100644 index 0000000..fe1b70a --- /dev/null +++ b/src/config/replacer/index.js @@ -0,0 +1,10 @@ +/** + * webpack-theme-color-replacer 配置 + * webpack-theme-color-replacer 是一个高效的主题色替换插件,可以实现系统运行时动态切换主题功能。 + * 但有些情景下,我们需要为 webpack-theme-color-replacer 配置一些规则,以达到我们的个性化需求的目的 + * + * @cssResolve: css处理规则,在 webpack-theme-color-replacer 提取 需要替换主题色的 css 后,应用此规则。一般在 + * webpack-theme-color-replacer 默认规则无法达到我们的要求时使用。 + */ +const cssResolve = require('./resolve.config') +module.exports = {cssResolve} diff --git a/src/config/replacer/resolve.config.js b/src/config/replacer/resolve.config.js new file mode 100644 index 0000000..b3c49de --- /dev/null +++ b/src/config/replacer/resolve.config.js @@ -0,0 +1,38 @@ +/** + * webpack-theme-color-replacer 插件的 resolve 配置 + * 为特定的 css 选择器(selector)配置 resolve 规则。 + * + * key 为 css selector 值或合法的正则表达式字符串 + * 当 key 设置 css selector 值时,会匹配对应的 css + * 当 key 设置为正则表达式时,会匹配所有满足此正则表达式的的 css + * + * value 可以设置为 boolean 值 false 或 一个对象 + * 当 value 为 false 时,则会忽略此 css,即此 css 不纳入 webpack-theme-color-replacer 管理 + * 当 value 为 对象时,会调用该对象的 resolve 函数,并传入 cssText(原始的 css文本) 和 cssObj(css对象)参数; resolve函数应该返 + * 回一个处理后的、合法的 css字符串(包含 selector) + * 注意: value 不能设置为 true + */ +const cssResolve = { + '.ant-checkbox-checked .ant-checkbox-inner::after': false, + '.ant-menu-dark .ant-menu-inline.ant-menu-sub': { + resolve(cssText, cssObj) { + cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1) + return cssObj.toText() + } + }, + '.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu:hover,.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-submenu-selected': { + resolve(cssText, cssObj) { + cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)') + return cssObj.toText() + } + }, + '.ant-layout-sider': { + resolve(cssText, cssObj) { + cssObj.selector = '.ant-layout-sider-dark' + return cssObj.toText() + } + }, + '/keyframes/': false +} + +module.exports = cssResolve diff --git a/src/theme/default/color.less b/src/theme/default/color.less index 4aeb66c..cb94b60 100644 --- a/src/theme/default/color.less +++ b/src/theme/default/color.less @@ -17,6 +17,7 @@ @title-color: @heading-color; @text-color: @text-color; +@text-color-inverse: @text-color-inverse; @text-color-second: @text-color-secondary; @base-bg-color: @body-background; @bg-color: @layout-body-background; diff --git a/src/utils/colors.js b/src/utils/colors.js index a8a2e03..841bfa4 100644 --- a/src/utils/colors.js +++ b/src/utils/colors.js @@ -1,25 +1,47 @@ const varyColor = require('webpack-theme-color-replacer/client/varyColor') +const generate = require('@ant-design/colors/lib/generate').default +const {theme} = require('../config/default') +const themeMode = theme.mode + // ant design vue 默认主题色 -const antPrimaryColor = '#1890ff' -// ant design vue 默认dark主题色,若主题色为默认主题色则返回此 dark 主题色系 -const antDarkColors = ['#000c17', '#001529', '#002140'] -const nightColors = ['#151515', '#1f1f1f', '#1f1f1f'] +const antdPrimary = '#1890ff' +// 获取 ant design 色系 +function getAntdColors(color, mode) { + let options = mode && (mode == themeMode.NIGHT) ? {theme: 'dark'} : undefined + return generate(color, options) +} -function getDarkColors(color, theme) { - if (theme == 'night') { - return nightColors +// 获取菜单色系 +function getMenuColors(color, mode) { + if (mode == themeMode.NIGHT) { + return ['#151515', '#1f1f1f', '#1e1e1e'] + } else if (color == antdPrimary) { + return ['#000c17', '#001529', '#002140'] + } else { + return [varyColor.darken(color, 0.93), varyColor.darken(color, 0.83), varyColor.darken(color, 0.73)] } - if (color == antPrimaryColor) { - return antDarkColors - } - const darkColors = [] - darkColors.push(varyColor.darken(color, 0.93), varyColor.darken(color, 0.83), varyColor.darken(color, 0.73)) - return darkColors } -function getBgColors(theme) { - return theme == 'light' ? ['#f0f2f5', '#ffffff'] : ['#000000', '#141414'] +// 获取主题模式切换色系 +function getThemeToggleColors(color, mode) { + //主色系 + const mainColors = getAntdColors(color, mode) + const primary = mainColors[5] + //辅助色系,因为 antd 目前没针对夜间模式设计,所以增加辅助色系以保证夜间模式的正常切换 + const subColors = getAntdColors(primary, themeMode.LIGHT) + //菜单色系 + const menuColors = getMenuColors(color, mode) + //内容色系(包含背景色、文字颜色等) + const themeCfg = theme[mode] + let contentColors = Object.keys(themeCfg) + .map(key => themeCfg[key]) + .map(color => isHex(color) ? color : toNum3(color).join(',')) + // 内容色去重 + // contentColors = [...new Set(contentColors)] + // rgb 格式的主题色 + let rgbColors = [toNum3(primary).join(',')] + return {primary, mainColors, subColors, menuColors, contentColors, rgbColors} } function toNum3(color) { @@ -51,4 +73,12 @@ function isRgba(color) { return color.length >= 13 && color.slice(0, 4) == 'rgba' } -module.exports = {getDarkColors, getBgColors, isHex, isRgb, isRgba, toNum3} +module.exports = { + isHex, + isRgb, + isRgba, + toNum3, + getAntdColors, + getMenuColors, + getThemeToggleColors +} diff --git a/src/utils/theme-color-replacer-extend.js b/src/utils/theme-color-replacer-extend.js new file mode 100644 index 0000000..7e586dc --- /dev/null +++ b/src/utils/theme-color-replacer-extend.js @@ -0,0 +1,89 @@ +const {cssResolve} = require('../config/replacer') +// 修正 webpack-theme-color-replacer 插件提取的 css 结果 +function resolveCss(output, srcArr) { + let regExps = [] + // 提取 resolve 配置中所有的正则配置 + Object.keys(cssResolve).forEach(key => { + let isRegExp = false + let reg = {} + try { + reg = eval(key) + isRegExp = reg instanceof RegExp + } catch (e) { + isRegExp = false + } + if (isRegExp) { + regExps.push([reg, cssResolve[key]]) + } + }) + + // 去重 + srcArr = dropDuplicate(srcArr) + + // 处理 css + let outArr = [] + srcArr.forEach(text => { + // 转换为 css 对象 + let cssObj = parseCssObj(text) + // 根据selector匹配配置,匹配成功,则按配置处理 css + if (cssResolve[cssObj.selector]) { + outArr.push(cssResolve[cssObj.selector].resolve(text, cssObj)) + } else { + let cssText = '' + // 匹配不成功,则测试是否有匹配的正则配置,有则按正则对应的配置处理 + for (let regExp of regExps) { + if (regExp[0].test(cssObj.selector)) { + let cssCfg = regExp[1] + cssText = cssCfg ? cssCfg.resolve(text, cssObj) : '' + break + } + // 未匹配到正则,则设置 cssText 为默认的 css(即不处理) + cssText = text + } + if (cssText != '') { + outArr.push(cssText) + } + } + }) + output = outArr.join('\n') + return output +} + +// 数组去重 +function dropDuplicate(arr) { + let map = {} + let r = [] + for (let s of arr) { + if (!map[s]) { + r.push(s) + map[s] = 1 + } + } + return r +} + +/** + * 从字符串解析 css 对象 + * @param cssText + * @returns {{ + * name: String, + * rules: Array[String], + * toText: function + * }} + */ +function parseCssObj(cssText) { + let css = {} + const ruleIndex = cssText.indexOf('{') + css.selector = cssText.substring(0, ruleIndex) + const ruleBody = cssText.substring(ruleIndex + 1, cssText.length - 1) + const rules = ruleBody.split(';') + css.rules = rules + css.toText = function () { + let body = '' + this.rules.forEach(item => {body += item + ';'}) + return `${this.selector}{${body}}` + } + return css +} + +module.exports = {resolveCss} diff --git a/src/utils/themeUtil.js b/src/utils/themeUtil.js index 6da2bae..a532f6d 100644 --- a/src/utils/themeUtil.js +++ b/src/utils/themeUtil.js @@ -1,80 +1,40 @@ -// const varyColor = require('webpack-theme-color-replacer/client/varyColor') const client = require('webpack-theme-color-replacer/client') -const generate = require('@ant-design/colors/lib/generate').default const {theme, themeColor} = require('../config') -const {getDarkColors, isHex, toNum3} = require('../utils/colors') -const themeCfg = require('../config/default').theme +const {getMenuColors, getAntdColors, getThemeToggleColors} = require('../utils/colors') +const {theme: themeCfg} = require('../config/default') module.exports = { - primaryColor: themeColor, getThemeColors(color, $theme) { - let _theme = $theme || theme - let opts = (_theme == 'night') ? {theme: 'dark'} : undefined - let palettes = generate(color, opts) - const primary = palettes[5] - palettes = palettes.concat(generate(primary)) - console.log(palettes) - const darkBgColors = getDarkColors(color, _theme) - const _themeCfg = themeCfg[_theme] - const bgColors = Object.keys(_themeCfg) - .map(key => _themeCfg[key]) - .map(color => isHex(color) ? color : toNum3(color).join(',')) - let rgb = toNum3(primary).join(',') - return palettes.concat(darkBgColors).concat(bgColors).concat(rgb) + const _color = color || themeColor + const _theme = $theme || theme + const replaceColors = getThemeToggleColors(_color, _theme) + const themeColors = [ + ...replaceColors.mainColors, + ...replaceColors.subColors, + ...replaceColors.menuColors, + ...replaceColors.contentColors, + ...replaceColors.rgbColors + ] + return themeColors }, changeThemeColor (newColor, $theme) { - let options = { - newColors: this.getThemeColors(newColor, $theme) - } - let promise = client.changer.changeColor(options) + let promise = client.changer.changeColor({newColors: this.getThemeColors(newColor, $theme)}) return promise }, - changeSelector (selector) { - switch (selector) { - case '.ant-layout-sider': - return '.ant-layout-sider-dark' - case '.ant-menu-dark .ant-menu-inline.ant-menu-sub': - return '.ant-menu-dark .ant-menu-inline:not(.ant-menu-sub)' - case '.ant-checkbox-checked .ant-checkbox-inner::after': - return '.ant-checkbox-checked :not(.ant-checkbox-inner)::after' - case '.side-menu .logo h1': - return '.side-menu .logo :not(h1)' - case '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': - case '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal > .ant-menu-submenu-selected,.ant-menu-horizontal > .ant-menu-submenu:hover': - return '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu:hover' - case '.ant-menu-horizontal > .ant-menu-item-selected > a': - case '.ant-menu-horizontal>.ant-menu-item-selected>a': - return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item-selected > a' - case '.ant-menu-horizontal > .ant-menu-item > a:hover': - case '.ant-menu-horizontal>.ant-menu-item>a:hover': - return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item > a:hover' - default : - return selector - } - }, modifyVars(color) { - let opts = (theme == 'night') ? {theme: 'dark'} : undefined - const darkColors = getDarkColors(color, theme) - const palettes = generate(color, opts) + let _color = color || themeColor + const palettes = getAntdColors(_color, theme) + const menuColors = getMenuColors(_color, theme) + const primary = palettes[5] return { - 'primary-color': palettes[5], - 'primary-1': palettes[0], - 'primary-2': palettes[1], - 'primary-3': palettes[2], - 'primary-4': palettes[3], - 'primary-5': palettes[4], - 'primary-6': palettes[5], - 'primary-7': palettes[6], - 'primary-8': palettes[7], - 'primary-9': palettes[8], - 'primary-10': palettes[9], - 'info-color': palettes[5], + 'primary-color': primary, + 'info-color': primary, 'alert-info-bg-color': palettes[0], 'alert-info-border-color': palettes[3], - 'processing-color': palettes[5], - 'menu-dark-submenu-bg': darkColors[0], - 'layout-header-background': darkColors[1], - 'layout-trigger-background': darkColors[2], + 'processing-color': primary, + 'menu-dark-submenu-bg': menuColors[0], + 'layout-header-background': menuColors[1], + 'layout-trigger-background': menuColors[2], ...themeCfg[theme] } } diff --git a/vue.config.js b/vue.config.js index e894155..80339af 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,7 +1,7 @@ let path = require('path') const ThemeColorReplacer = require('webpack-theme-color-replacer') -const {getThemeColors, changeSelector, modifyVars} = require('./src/utils/themeUtil') -const themeColor = require('./src/config').themeColor +const {getThemeColors, modifyVars} = require('./src/utils/themeUtil') +const {resolveCss} = require('./src/utils/theme-color-replacer-extend') module.exports = { pluginOptions: { @@ -15,24 +15,26 @@ module.exports = { config.plugins.push( new ThemeColorReplacer({ fileName: 'css/theme-colors-[contenthash:8].css', - matchColors: getThemeColors(themeColor), - changeSelector + matchColors: getThemeColors(), + resolveCss }) ) }, chainWebpack: config => { - config - .plugin('optimize-css') - .tap(args => { - args[0].cssnanoOptions.preset[1].colormin = false - return args - }) + // 生产环境下关闭css压缩的 colormin 项,因为此项优化与主题色替换功能冲突 + if (process.env.NODE_ENV === 'production') { + config.plugin('optimize-css') + .tap(args => { + args[0].cssnanoOptions.preset[1].colormin = false + return args + }) + } }, css: { loaderOptions: { less: { lessOptions: { - modifyVars: modifyVars(themeColor), + modifyVars: modifyVars(), javascriptEnabled: true } }