基本思路
實現思路:實現一個mixins混入的主題js即theme.js,注冊到全局下。使用el-color-picker組件切換顏色的時候,把顏色值傳遞到根root下,在根實例下監聽主題色的變化來更改頁面的主題,然后所有具體的路由頁面的主題色修改通過在APP.vue頁面監聽路由變化來調用改變主題色方法。這里面用到providey與inject的使用,以及怎樣把設置的主題色傳遞到根節點下,這里使用了vue1.x中的dispatch方法。
大致總結:
- 1.把得到的主題色傳遞給根root實例,在根實例中監聽主題色的變化,並調用setThemeColor(newval, oldval)方法;
- 2.在APP.vue中監聽路由變化,並調用setThemeColor(newval, oldval)方法,目的是進入具體路由頁面需要修改頁面的head中的style樣式、DOM元素中的行內style樣式;
具體實現如下。
整體效果
先看下整體實現效果:
效果預覽地址:《vue+element-ui動態設置主題效果》
使用方式
設置element-ui主題色引入到main.js中
在src/styles下新建element-variables.scss
:
/* 改變主題色變量 */ $--color-primary: #42b983; /* 改變 icon 字體路徑變量,必需 */ $--font-path: '~element-ui/lib/theme-chalk/fonts'; @import "~element-ui/packages/theme-chalk/src/index"; :export { colorPrimary: $--color-primary }
在main.js中引入該css:
import variables from '@/styles/element-variables.scss'
全局混入theme.js、emitter.js
- theme.js主要方法
setThemeColor(newval, oldval)
,該方法傳入新的顏色值與舊的顏色值; - emitter.js中使用
$$dispatch
方法把修改的主題色提交到根實例下;
在main.js 中引入該兩個JS並注冊:
import theme from '@/mixins/theme.js' import emitter from '@/mixins/emitter.js' Vue.mixin(theme) Vue.mixin(emitter)
核心代碼調用
- 主題色提交到根實例代碼以及監聽具體的路由頁面修改樣式(APP.vue)
export default { name: 'App', inject: { themeConfig: { default: () => ({ themeColor: '', defaultColor: '' }) } }, data() { return { themeColor: '' } }, watch: { $route() { // 關鍵作用-進入到具體路由頁面更新頁面中DOM樣式 if (typeof this.themeConfig.themeColor != 'undefined' && this.themeConfig.themeColor !== this.themeConfig.defaultColor) { this.$nextTick(() => { if (this.themeConfig.themeColor && this.themeConfig.defaultColor) { this.setThemeColor(this.themeConfig.themeColor, this.themeConfig.defaultColor) } }) } } }, created() { // 如果本地存在主題色從本地獲取,並提交給root分發到頁面進行渲染 if(Cookies.get('themeColor')) { this.themeColor = Cookies.get('themeColor'); this.$$dispatch('root','root.config',[this.themeColor,true]); // 傳遞數組-解決初始加載執行setThemeColor兩次問題 } else { this.themeColor = this.themeConfig.themeColor; } }, methods: { // 改變主題顏色 changeThemeColor(value) { this.$$dispatch('root','root.config',value); Cookies.set('themeColor', value, { path: '/' }); } } }
- 根實例監聽獲取的主題色並監聽設置主題色(main.js)
new Vue({ el: '#app', name: 'root', provide(){ return { themeConfig: this } }, data() { return { themeColor: variables.colorPrimary.toLowerCase(), defaultColor: variables.colorPrimary.toLowerCase(), themeFirstLoaded: true, // 主題是否第一次加載,解決初始主題watch跟$route執行setThemeColor兩次問題 } }, created() { this.$on('root.config',(result,themeFirstLoaded) => { this.themeColor = result.toLowerCase(); this.themeFirstLoaded = themeFirstLoaded; }) }, watch: { themeColor(newval, oldval) { if(!this.themeFirstLoaded) { this.setThemeColor(newval, oldval); } } }, router, components: { App }, template: '<App/>' })
theme.js設置主題代碼
export default { methods: { // 樣式更新 updateStyle(stylecon, oldCulster, newCluster) { let newStyleCon = stylecon; oldCulster.forEach((color, index) => { let regexp = ''; if (color.split(',').length > 1) { const rgbArr = color.split(','); regexp = new RegExp("\\s*" + rgbArr[0] + "\\s*,\\s*" + rgbArr[1] + "\\s*,\\s*" + rgbArr[2] + "\\s*", 'ig'); } else { regexp = new RegExp(color, 'ig'); } newStyleCon = newStyleCon.replace(regexp, newCluster[index]) }) return newStyleCon; }, // 得到需要修改的一系類顏色值 getThemeCluster(theme) { const clusters = [theme]; for (let i = 0; i <= 9; i++) { clusters.push(this.getTintColor(theme, Number(i / 10).toFixed(2))); } clusters.push(this.getShadeColor(theme, 0.1)); return clusters; }, // 得到色調顏色 getTintColor(color, tint) { let red = parseInt(color.slice(0, 2), 16); let green = parseInt(color.slice(2, 4), 16); let blue = parseInt(color.slice(4, 6), 16); if (tint == 0) { return [red, green, blue].join(','); } else { red += Math.round((255 - red) * tint); green += Math.round((255 - green) * tint); blue += Math.round((255 - blue) * tint); red = red.toString(16); green = green.toString(16); blue = blue.toString(16); return `#${red}${green}${blue}` } }, // 獲取陰影色調顏色 getShadeColor(color, shade) { let red = parseInt(color.slice(0, 2), 16); let green = parseInt(color.slice(2, 4), 16); let blue = parseInt(color.slice(4, 6), 16); red = Math.round((1 - shade) * red); green = Math.round((1 - shade) * green); blue = Math.round((1 - shade) * blue); red = red.toString(16); green = green.toString(16); blue = blue.toString(16); return `#${red}${green}${blue}` }, // 獲取外鏈css文本內容 getCSSText(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { const styleText = xhr.responseText.replace(/@font-face{[^}]+}/, '') resolve(styleText); } } xhr.open('GET', url) xhr.send() }) }, // 獲取外鏈CSS樣式的url地址 getRequestUrl: function(src) { if (/^(http|https):\/\//g.test(src)) { return src; } let filePath = this.getFilePath(); let count = 0; const regexp = /\.\.\//g; while (regexp.exec(src)) { count++; } while (count--) { filePath = filePath.substring(0, filePath.lastIndexOf('/')); } return filePath + "/" + src.replace(/\.\.\//g, ""); }, // 獲取當前window的url地址 getFilePath: function() { const curHref = window.location.href; if (curHref.indexOf('/#/') != -1) { return curHref.substring(0, curHref.indexOf('/#/')); } else { return curHref.substring(0, curHref.lastIndexOf('/') + 1); } }, // 修改主題色-head樣式以及DOM行內樣式 async setThemeColor(newval, oldval) { if (typeof newval !== 'string') return; const newThemeCluster = this.getThemeCluster(newval.replace('#', '')); const orignalCluster = this.getThemeCluster(oldval.replace('#', '')); // 獲取原始值中包含rgb格式的值存為數組 const rgbArr = orignalCluster[1].split(','); const orignalRGBRegExp = new RegExp("\\(\\s*" + rgbArr[0] + "\\s*,\\s*" + rgbArr[1] + "\\s*,\\s*" + rgbArr[2] + "\\s*\\)", 'i'); // 獲取外鏈的樣式內容並替換樣式 let styleTag = document.getElementById('new-configTheme__styles'); const tagsDom = document.getElementsByTagName('link'); if (!styleTag && tagsDom.length) { styleTag = document.createElement('style') styleTag.setAttribute('id', 'new-configTheme__styles') document.head.appendChild(styleTag); const tagsDomList = Array.prototype.slice.call(tagsDom); let innerTextCon = ''; for (let i = 0; i < tagsDomList.length; i++) { const value = tagsDomList[i]; const tagAttributeSrc = value.getAttribute('href'); const requestUrl = this.getRequestUrl(tagAttributeSrc); const styleCon = await this.getCSSText(requestUrl); if (new RegExp(oldval, 'i').test(styleCon) || orignalRGBRegExp.test(styleCon)) { innerTextCon += this.updateStyle(styleCon, orignalCluster, newThemeCluster); } } styleTag.innerText = innerTextCon; } // 獲取頁面的style標簽 const styles = [].slice.call(document.querySelectorAll('style')).filter((style) => { const text = style.innerText; return new RegExp(oldval, 'i').test(text) || orignalRGBRegExp.test(text); }) // 獲取頁面的style標簽內容,使用updateStyle直接更新即可 styles.forEach((style) => { const { innerText } = style; if (typeof innerText !== 'string') return; style.innerText = this.updateStyle(innerText, orignalCluster, newThemeCluster); }) // 獲取DOM元素上的style const domAll = [].slice.call(document.getElementsByTagName('*')).filter((dom, index) => { const stylCon = dom.getAttribute('style'); return stylCon && (new RegExp(oldval, 'i').test(stylCon) || orignalRGBRegExp.test(stylCon)) }) domAll.forEach((dom) => { const styleCon = dom.getAttribute('style'); dom.style = this.updateStyle(styleCon, orignalCluster, newThemeCluster); }) } } }
主要思路:通過傳入新、舊顏色值替換head標簽中樣式以及DOM元素style的行內元素的樣式。
重要:外鏈的樣式最好是壓縮的樣式,比如在vue-cli腳手架中,本地開發環境需要把樣式提取到一個文件,並且壓縮,dev.config.js部分代碼如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin') // 提取CSS const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') // 壓縮CSS const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap,extract: true, usePostCSS: true }) }, plugins: [ // ...省略其他代碼 new ExtractTextPlugin({ filename: 'bundle.css', allChunks: true }), new OptimizeCSSPlugin({ cssProcessorOptions: config.build.productionSourceMap ? { safe: true, map: { inline: false } } : { safe: true } }) ] })
github示例源碼地址:《vue+element-ui動態主題色設置》