diff --git a/package.json b/package.json index b479d6f..0a19077 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@vueuse/motion": "^2.0.0-beta.4", "animate.css": "^4.1.1", "axios": "^0.21.1", + "css-color-function": "^1.3.3", "dayjs": "^1.10.7", "element-plus": "1.2.0-beta.6", "element-resize-detector": "^1.2.3", @@ -52,6 +53,7 @@ "remixicon": "^2.5.0", "resize-observer-polyfill": "^1.5.1", "responsive-storage": "^1.0.11", + "rgb-hex": "^4.0.0", "vue": "^3.2.24", "vue-i18n": "^9.2.0-beta.3", "vue-router": "^4.0.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a061ad3..0ccf53d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,7 @@ specifiers: axios: ^0.21.1 babel-plugin-transform-remove-console: 6.9.4 cross-env: 7.0.3 + css-color-function: ^1.3.3 dayjs: ^1.10.7 element-plus: 1.2.0-beta.6 element-resize-detector: ^1.2.3 @@ -53,6 +54,7 @@ specifiers: remixicon: ^2.5.0 resize-observer-polyfill: ^1.5.1 responsive-storage: ^1.0.11 + rgb-hex: ^4.0.0 rimraf: 3.0.2 sass: ^1.45.0 sass-loader: ^12.3.0 @@ -82,6 +84,7 @@ dependencies: "@vueuse/motion": 2.0.0-beta.4_vue@3.2.24 animate.css: 4.1.1 axios: 0.21.4 + css-color-function: 1.3.3 dayjs: 1.10.7 element-plus: 1.2.0-beta.6_vue@3.2.24 element-resize-detector: 1.2.3 @@ -97,6 +100,7 @@ dependencies: remixicon: 2.5.0 resize-observer-polyfill: 1.5.1 responsive-storage: 1.0.11_vue@3.2.24 + rgb-hex: 4.0.0 vue: 3.2.24 vue-i18n: 9.2.0-beta.17_vue@3.2.24 vue-router: 4.0.12_vue@3.2.24 @@ -1867,6 +1871,10 @@ packages: } dev: true + /balanced-match/0.1.0: + resolution: { integrity: sha1-tQS9BYabOSWd0MXvw12EMXbczEo= } + dev: false + /balanced-match/1.0.2: resolution: { @@ -2192,6 +2200,11 @@ packages: is-regexp: 2.1.0 dev: true + /clone/1.0.4: + resolution: { integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4= } + engines: { node: ">=0.8" } + dev: false + /clone/2.1.2: resolution: { integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= } engines: { node: ">=0.8" } @@ -2204,7 +2217,6 @@ packages: } dependencies: color-name: 1.1.3 - dev: true /color-convert/2.0.1: resolution: @@ -2218,14 +2230,18 @@ packages: /color-name/1.1.3: resolution: { integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= } - dev: true /color-name/1.1.4: resolution: { integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== } - dev: true + + /color-string/0.3.0: + resolution: { integrity: sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= } + dependencies: + color-name: 1.1.4 + dev: false /color-string/1.9.0: resolution: @@ -2237,6 +2253,14 @@ packages: simple-swizzle: 0.2.2 dev: true + /color/0.11.4: + resolution: { integrity: sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= } + dependencies: + clone: 1.0.4 + color-convert: 1.9.3 + color-string: 0.3.0 + dev: false + /color/4.1.0: resolution: { @@ -2422,6 +2446,15 @@ packages: which: 2.0.2 dev: true + /css-color-function/1.3.3: + resolution: { integrity: sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4= } + dependencies: + balanced-match: 0.1.0 + color: 0.11.4 + debug: 3.2.7 + rgb: 0.1.0 + dev: false + /css-declaration-sorter/6.1.3_postcss@8.3.11: resolution: { @@ -2602,6 +2635,15 @@ packages: ms: 2.0.0 dev: true + /debug/3.2.7: + resolution: + { + integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + } + dependencies: + ms: 2.1.2 + dev: false + /debug/4.3.2: resolution: { @@ -4731,7 +4773,6 @@ packages: { integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== } - dev: true /multimatch/4.0.0: resolution: @@ -6016,6 +6057,19 @@ packages: engines: { iojs: ">=1.0.0", node: ">=0.10.0" } dev: true + /rgb-hex/4.0.0: + resolution: + { + integrity: sha512-Eg2ev5CiMBnQ9Gpflmqbwbso0CCdISqtVIow7OpYSLN1ULUv2jTB9YieS1DSSn/17AD7KkPWDPzSFzI4GSuu/Q== + } + engines: { node: ">=12" } + dev: false + + /rgb/0.1.0: + resolution: { integrity: sha1-vieykej+/+rBvZlylyG/pA/AN7U= } + hasBin: true + dev: false + /rimraf/3.0.2: resolution: { diff --git a/src/layout/components/setting/index.vue b/src/layout/components/setting/index.vue index 74eb7bc..dd39f7e 100644 --- a/src/layout/components/setting/index.vue +++ b/src/layout/components/setting/index.vue @@ -9,6 +9,8 @@ import { useCssModule, getCurrentInstance } from "vue"; +import rgbHex from "rgb-hex"; +import { find } from "lodash-es"; import panel from "../panel/index.vue"; import { getConfig } from "/@/config"; import { useRouter } from "vue-router"; @@ -19,8 +21,10 @@ import { debounce } from "/@/utils/debounce"; import darkIcon from "/@/assets/svg/dark.svg"; import { themeColorsType } from "../../types"; import { useAppStoreHook } from "/@/store/modules/app"; +import { useEpThemeStoreHook } from "/@/store/modules/epTheme"; import { storageLocal, storageSession } from "/@/utils/storage"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; +import { createNewStyle, writeNewStyle } from "/@/utils/theme"; import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils"; const router = useRouter(); @@ -77,6 +81,8 @@ const markValue = ref(storageLocal.getItem("showModel") || "smart"); const logoVal = ref(storageLocal.getItem("logoVal") || "1"); +const epThemeColor = ref(useEpThemeStoreHook().getEpThemeColor); + const settings = reactive({ greyVal: instance.sets.grey, weakVal: instance.sets.weak, @@ -146,6 +152,8 @@ nextTick(() => { settings.weakVal && document.querySelector("html")?.setAttribute("class", "html-weakness"); settings.tabsVal && tagsChange(); + + writeNewStyle(createNewStyle(epThemeColor.value)); }); // 清空缓存并返回登录页 @@ -167,6 +175,7 @@ function onReset() { } ]); useMultiTagsStoreHook().multiTagsCacheChange(getConfig().MultiTagsCache); + useEpThemeStoreHook().setEpThemeColor("#409EFF"); router.push("/login"); } @@ -236,8 +245,22 @@ function setLayoutThemeColor(theme: string) { scopeName: `layout-theme-${theme}` }); instance.layout = { layout: useAppStoreHook().layout, theme }; + + if (theme === "default" || theme === "light") { + setEpThemeColor("#409EFF"); + } else { + const colors = find(themeColors.value, { themeColor: theme }); + const color = "#" + rgbHex(colors.rgb); + setEpThemeColor(color); + } } +// 设置ep主题色 +const setEpThemeColor = (color: string) => { + writeNewStyle(createNewStyle(color)); + useEpThemeStoreHook().setEpThemeColor(color); +}; + let dataTheme = ref(false); // 日间、夜间主题切换 diff --git a/src/router/index.ts b/src/router/index.ts index d2fbc11..74b04bf 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -89,7 +89,7 @@ router.beforeEach((to: toRouteType, _from, next) => { }); }; // 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对静态路由) - if (to.meta?.realPath) { + if (to.meta?.dynamicLevel) { const routes = router.options.routes; const { refreshRedirect } = to.meta; const { name, meta } = findRouteByPath(refreshRedirect, routes); diff --git a/src/router/types.ts b/src/router/types.ts index 0577e04..fa4e103 100644 --- a/src/router/types.ts +++ b/src/router/types.ts @@ -4,6 +4,6 @@ export interface toRouteType extends RouteLocationNormalized { meta: { keepAlive: boolean; refreshRedirect: string; - realPath: string; + dynamicLevel: string; }; } diff --git a/src/router/utils.ts b/src/router/utils.ts index af9f6ed..46de57e 100644 --- a/src/router/utils.ts +++ b/src/router/utils.ts @@ -216,11 +216,7 @@ const addAsyncRoutes = (arrRoutes: Array) => { if (v.redirect) { v.component = Layout; } else { - if (v.meta.realPath) { - v.component = modulesRoutes[`/src/views${v.meta.realPath}/index.vue`]; - } else { - v.component = modulesRoutes[`/src/views${v.path}/index.vue`]; - } + v.component = modulesRoutes[`/src/views${v.path}/index.vue`]; } if (v.children) { addAsyncRoutes(v.children); diff --git a/src/store/modules/epTheme.ts b/src/store/modules/epTheme.ts new file mode 100644 index 0000000..0632036 --- /dev/null +++ b/src/store/modules/epTheme.ts @@ -0,0 +1,25 @@ +import { store } from "/@/store"; +import { defineStore } from "pinia"; +import { storageLocal } from "/@/utils/storage"; + +export const useEpThemeStore = defineStore({ + id: "pure-epTheme", + state: () => ({ + epThemeColor: storageLocal.getItem("epThemeColor") || "#409EFF" + }), + getters: { + getEpThemeColor() { + return this.epThemeColor; + } + }, + actions: { + setEpThemeColor(newColor) { + this.epThemeColor = newColor; + storageLocal.setItem("epThemeColor", newColor); + } + } +}); + +export function useEpThemeStoreHook() { + return useEpThemeStore(store); +} diff --git a/src/store/modules/multiTags.ts b/src/store/modules/multiTags.ts index 4ad0529..64a31f5 100644 --- a/src/store/modules/multiTags.ts +++ b/src/store/modules/multiTags.ts @@ -55,31 +55,30 @@ export const useMultiTagsStore = defineStore({ case "push": { const tagVal = value as multiType; + const tagPath = tagVal?.path; // 判断tag是否已存在 const tagHasExits = this.multiTags.some(tag => { - return tag.path === tagVal?.path; + return tag.path === tagPath; }); // 判断tag中的query键值是否相等 const tagQueryHasExits = this.multiTags.some(tag => { - return isEqual(tag.query, tagVal.query); + return isEqual(tag.query, tagVal?.query); }); if (tagHasExits && tagQueryHasExits) return; - const meta = tagVal?.meta; - const dynamicLevel = meta?.dynamicLevel ?? -1; + const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1; if (dynamicLevel > 0) { // dynamicLevel动态路由可打开的数量 - const realPath = meta?.realPath ?? ""; // 获取到已经打开的动态路由数, 判断是否大于dynamicLevel if ( - this.multiTags.filter(e => e.meta?.realPath ?? "" === realPath) - .length >= dynamicLevel + this.multiTags.filter(e => e?.path === tagPath).length >= + dynamicLevel ) { // 关闭第一个 const index = this.multiTags.findIndex( - item => item.meta?.realPath === realPath + item => item?.path === tagPath ); index !== -1 && this.multiTags.splice(index, 1); } diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 608bc30..9094edf 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -1,6 +1,7 @@ import { defineStore } from "pinia"; import { store } from "/@/store"; import { cacheType } from "./types"; +import { cloneDeep } from "lodash-es"; import { RouteConfigs } from "/@/layout/types"; import { constantMenus } from "/@/router/modules"; import { ascending, filterTree } from "/@/router/utils"; @@ -12,6 +13,8 @@ export const usePermissionStore = defineStore({ constantMenus, // 整体路由生成的菜单(静态、动态) wholeMenus: [], + // 深拷贝一个菜单树,与导航菜单不突出 + menusTree: [], buttonAuth: [], // 缓存页面keepAlive cachePageList: [] @@ -24,6 +27,10 @@ export const usePermissionStore = defineStore({ ascending(this.constantMenus.concat(routes)) ); + this.menusTree = cloneDeep( + filterTree(ascending(this.constantMenus.concat(routes))) + ); + const getButtonAuth = (arrRoutes: Array) => { if (!arrRoutes || !arrRoutes.length) return; arrRoutes.forEach((v: RouteConfigs) => { diff --git a/src/store/modules/types.ts b/src/store/modules/types.ts index 644ce86..c5eb062 100644 --- a/src/store/modules/types.ts +++ b/src/store/modules/types.ts @@ -25,8 +25,8 @@ export type multiType = { path: string; parentPath: string; name: string; - query: object; meta: any; + query?: object; }; export type setType = { diff --git a/src/utils/theme/index.ts b/src/utils/theme/index.ts new file mode 100644 index 0000000..936e8c4 --- /dev/null +++ b/src/utils/theme/index.ts @@ -0,0 +1,71 @@ +import rgbHex from "rgb-hex"; +import color from "css-color-function"; +import epCss from "element-plus/dist/index.css"; + +const formula = { + "shade-1": "color(primary shade(10%))", + "light-1": "color(primary tint(10%))", + "light-2": "color(primary tint(20%))", + "light-3": "color(primary tint(30%))", + "light-4": "color(primary tint(40%))", + "light-5": "color(primary tint(50%))", + "light-6": "color(primary tint(60%))", + "light-7": "color(primary tint(70%))", + "light-8": "color(primary tint(80%))", + "light-9": "color(primary tint(90%))" +}; + +export const writeNewStyle = newStyle => { + const style = window.document.createElement("style"); + style.innerText = newStyle; + window.document.head.appendChild(style); +}; + +export const createNewStyle = primaryStyle => { + const colors = createColors(primaryStyle); + let cssText = getOriginStyle(); + Object.keys(colors).forEach(key => { + cssText = cssText.replace( + new RegExp("(:|\\s+)" + key, "g"), + "$1" + colors[key] + ); + }); + return cssText; +}; + +export const createColors = primary => { + if (!primary) return; + const colors = { + primary + }; + Object.keys(formula).forEach(key => { + const value = formula[key].replace(/primary/, primary); + colors[key] = "#" + rgbHex(color.convert(value)); + }); + return colors; +}; + +export const getOriginStyle = () => { + return getStyleTemplate(epCss); +}; + +const getStyleTemplate = data => { + const colorMap = { + "#3a8ee6": "shade-1", + "#409eff": "primary", + "#53a8ff": "light-1", + "#66b1ff": "light-2", + "#79bbff": "light-3", + "#8cc5ff": "light-4", + "#a0cfff": "light-5", + "#b3d8ff": "light-6", + "#c6e2ff": "light-7", + "#d9ecff": "light-8", + "#ecf5ff": "light-9" + }; + Object.keys(colorMap).forEach(key => { + const value = colorMap[key]; + data = data.replace(new RegExp(key, "ig"), value); + }); + return data; +};