From 56996ff213500b5b85866b78d56b7b13d48b1930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=9D=E5=85=88=E7=91=9E?= <1490493387@qq.com> Date: Thu, 22 Feb 2024 23:09:41 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20:recycle:=20=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E8=89=B2=E3=80=81=E4=B8=BB=E9=A2=98=E3=80=81=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E7=AD=89=E8=AE=BE=E7=BD=AE=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LangSelect/index.vue | 45 +-- src/components/SizeSelect/index.vue | 42 +-- src/enums/LayoutEnum.ts | 18 ++ src/enums/ThemeEnum.ts | 18 ++ .../NavBar/components/NavbarRight.vue | 25 +- .../Settings/components/LayoutSelect.vue | 108 +++++++ .../Settings/components/ThemeColorPicker.vue | 41 +++ src/layout/components/Settings/index.vue | 239 +++++---------- src/layout/index.vue | 36 +-- src/store/modules/settings.ts | 32 +- src/utils/color.ts | 287 ++++++++++++++++++ src/utils/theme.ts | 70 ----- 12 files changed, 655 insertions(+), 306 deletions(-) create mode 100644 src/enums/LayoutEnum.ts create mode 100644 src/enums/ThemeEnum.ts create mode 100644 src/layout/components/Settings/components/LayoutSelect.vue create mode 100644 src/layout/components/Settings/components/ThemeColorPicker.vue create mode 100644 src/utils/color.ts delete mode 100644 src/utils/theme.ts diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue index 13feaf4..993af5d 100644 --- a/src/components/LangSelect/index.vue +++ b/src/components/LangSelect/index.vue @@ -1,25 +1,7 @@ - - + + diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue index f0800ba..4ea6062 100644 --- a/src/components/SizeSelect/index.vue +++ b/src/components/SizeSelect/index.vue @@ -1,24 +1,7 @@ - - + + diff --git a/src/enums/LayoutEnum.ts b/src/enums/LayoutEnum.ts new file mode 100644 index 0000000..dd9b043 --- /dev/null +++ b/src/enums/LayoutEnum.ts @@ -0,0 +1,18 @@ +/** + * 菜单布局枚举 + */ +export enum LayoutEnum { + /** + * 左侧菜单布局 + */ + LEFT = "left", + /** + * 顶部菜单布局 + */ + TOP = "top", + + /** + * 混合菜单布局 + */ + MIX = "mix", +} diff --git a/src/enums/ThemeEnum.ts b/src/enums/ThemeEnum.ts new file mode 100644 index 0000000..6102fa6 --- /dev/null +++ b/src/enums/ThemeEnum.ts @@ -0,0 +1,18 @@ +/** + * 主题枚举 + */ +export enum ThemeEnum { + /** + * 明亮主题 + */ + LIGHT = "light", + /** + * 暗黑主题 + */ + DARK = "dark", + + /** + * 系统自动 + */ + AUTO = "auto", +} diff --git a/src/layout/components/NavBar/components/NavbarRight.vue b/src/layout/components/NavBar/components/NavbarRight.vue index ce0f7f7..54170fa 100644 --- a/src/layout/components/NavBar/components/NavbarRight.vue +++ b/src/layout/components/NavBar/components/NavbarRight.vue @@ -5,14 +5,15 @@ - + - + @@ -31,23 +32,17 @@ @@ -103,6 +98,12 @@ function logout() { &:hover { background: rgb(0 0 0 / 10%); } + + .svg-icon, + svg, + .el-icon { + vertical-align: -0.15em; + } } .layout-top, diff --git a/src/layout/components/Settings/components/LayoutSelect.vue b/src/layout/components/Settings/components/LayoutSelect.vue new file mode 100644 index 0000000..0b880c1 --- /dev/null +++ b/src/layout/components/Settings/components/LayoutSelect.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/layout/components/Settings/components/ThemeColorPicker.vue b/src/layout/components/Settings/components/ThemeColorPicker.vue new file mode 100644 index 0000000..5a0d59c --- /dev/null +++ b/src/layout/components/Settings/components/ThemeColorPicker.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue index d9cc000..6627eda 100644 --- a/src/layout/components/Settings/index.vue +++ b/src/layout/components/Settings/index.vue @@ -1,6 +1,13 @@ +@/utils/color diff --git a/src/layout/index.vue b/src/layout/index.vue index 2d527ec..52dfd7f 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -3,14 +3,16 @@
+ +
- - +
- - - +
@@ -53,17 +51,11 @@ const appStore = useAppStore(); const settingsStore = useSettingsStore(); const permissionStore = usePermissionStore(); -const fixedHeader = computed(() => settingsStore.fixedHeader); -const showTagsView = computed(() => settingsStore.tagsView); -const layout = computed(() => settingsStore.layout); - -const activeTopMenuPath = computed(() => { - return appStore.activeTopMenuPath; -}); -// 混合模式左侧菜单 -const mixLeftMenus = computed(() => { - return permissionStore.mixLeftMenus; -}); +const fixedHeader = computed(() => settingsStore.fixedHeader); // 是否固定header +const showTagsView = computed(() => settingsStore.tagsView); // 是否显示tagsView +const layout = computed(() => settingsStore.layout); // 布局模式 left top mix +const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); // 顶部菜单激活path +const mixLeftMenus = computed(() => permissionStore.mixLeftMenus); // 混合布局左侧菜单 watch( () => activeTopMenuPath.value, @@ -209,7 +201,7 @@ function toggleSidebar() { height: 100%; padding-top: $navbar-height; - .sidebar-container__left { + .mix-container__left { position: relative; width: $sidebar-width; height: 100%; @@ -284,7 +276,7 @@ function toggleSidebar() { } .mix-container { - .sidebar-container__left { + .mix-container__left { width: $sidebar-width-collapsed; } } diff --git a/src/store/modules/settings.ts b/src/store/modules/settings.ts index 2d12d76..5e54d7c 100644 --- a/src/store/modules/settings.ts +++ b/src/store/modules/settings.ts @@ -35,8 +35,6 @@ export const useSettingsStore = defineStore("setting", () => { tagsView, sidebarLogo, layout, - themeColor, - theme, watermarkEnabled, }; @@ -50,21 +48,41 @@ export const useSettingsStore = defineStore("setting", () => { const setting = settingsMap[key]; if (setting) { setting.value = value; - // Special handling for theme changes - if (key === "theme") { - document.documentElement.classList.toggle("dark", value === "dark"); - } } } + /** + * 切换主题 + */ + function changeTheme(val: string) { + theme.value = val; + } + + /** + * 切换主题颜色 + */ + function changeThemeColor(val: string) { + themeColor.value = val; + } + + /** + * 切换布局 + */ + function changeLayout(val: string) { + layout.value = val; + } + return { tagsView, fixedHeader, sidebarLogo, layout, themeColor, - changeSetting, theme, watermarkEnabled, + changeSetting, + changeTheme, + changeThemeColor, + changeLayout, }; }); diff --git a/src/utils/color.ts b/src/utils/color.ts new file mode 100644 index 0000000..2503584 --- /dev/null +++ b/src/utils/color.ts @@ -0,0 +1,287 @@ +/** + * 颜色生成 + */ +type RGB = { + r: number; + g: number; + b: number; +}; +type HSL = { + h: number; + s: number; + l: number; +}; +type HEX = + | "0" + | "1" + | "2" + | "3" + | "4" + | "5" + | "6" + | "7" + | "8" + | "9" + | "A" + | "B" + | "C" + | "D" + | "E" + | "F"; + +const RGBUnit = 255; +const HEX_MAP: Record = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + A: 10, + B: 11, + C: 12, + D: 13, + E: 14, + F: 15, +}; +const rgbWhite = { + r: 255, + g: 255, + b: 255, +}; +const rgbBlack = { + r: 0, + g: 0, + b: 0, +}; + +/** + * RGB颜色转HSL颜色值 + * @param r 红色值 + * @param g 绿色值 + * @param b 蓝色值 + * @returns { h: [0, 360]; s: [0, 1]; l: [0, 1] } + */ +function rgbToHsl(rgb: RGB): HSL { + let { r, g, b } = rgb; + const hsl = { + h: 0, + s: 0, + l: 0, + }; + + // 计算rgb基数 ∈ [0, 1] + r /= RGBUnit; + g /= RGBUnit; + b /= RGBUnit; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + + // 计算h + if (max === min) { + hsl.h = 0; + } else if (max === r) { + hsl.h = 60 * ((g - b) / (max - min)) + (g >= b ? 0 : 360); + } else if (max === g) { + hsl.h = 60 * ((b - r) / (max - min)) + 120; + } else { + hsl.h = 60 * ((r - g) / (max - min)) + 240; + } + hsl.h = hsl.h > 360 ? hsl.h - 360 : hsl.h; + + // 计算l + hsl.l = (max + min) / 2; + + // 计算s + if (hsl.l === 0 || max === min) { + // 灰/白/黑 + hsl.s = 0; + } else if (hsl.l > 0 && hsl.l <= 0.5) { + hsl.s = (max - min) / (max + min); + } else { + hsl.s = (max - min) / (2 - (max + min)); + } + + return hsl; +} + +/** + * hsl -> rgb + * @param h [0, 360] + * @param s [0, 1] + * @param l [0, 1] + * @returns RGB + */ +function hslToRgb(hsl: HSL): RGB { + const { h, s, l } = hsl; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + const hUnit = h / 360; // 色相转换为 [0, 1] + + const Cr = fillCircleVal(hUnit + 1 / 3); + const Cg = fillCircleVal(hUnit); + const Cb = fillCircleVal(hUnit - 1 / 3); + + // 保持 [0, 1] 环状取值 + function fillCircleVal(val: number): number { + return val < 0 ? val + 1 : val > 1 ? val - 1 : val; + } + + function computedRgb(val: number): number { + let colorVal: number; + if (val < 1 / 6) { + colorVal = p + (q - p) * 6 * val; + } else if (val >= 1 / 6 && val < 1 / 2) { + colorVal = q; + } else if (val >= 1 / 2 && val < 2 / 3) { + colorVal = p + (q - p) * 6 * (2 / 3 - val); + } else { + colorVal = p; + } + return colorVal * 255; + } + + return { + r: Number(computedRgb(Cr).toFixed(0)), + g: Number(computedRgb(Cg).toFixed(0)), + b: Number(computedRgb(Cb).toFixed(0)), + }; +} + +/** + * 16进制颜色转换RGB + * @param color #rrggbb + * @returns RGB + */ +function hexToRGB(hex: string): RGB { + hex = hex.toUpperCase(); + const hexRegExp = /^#([0-9A-F]{6})$/; + if (!hexRegExp.test(hex)) { + throw new Error("请传入合法的16进制颜色值,eg: #FF0000"); + } + + const hexValArr = (hexRegExp.exec(hex)?.[1] || "000000").split( + "" + ) as Array; + + return { + r: HEX_MAP[hexValArr[0]] * 16 + HEX_MAP[hexValArr[1]], + g: HEX_MAP[hexValArr[2]] * 16 + HEX_MAP[hexValArr[3]], + b: HEX_MAP[hexValArr[4]] * 16 + HEX_MAP[hexValArr[5]], + }; +} + +/** + * rgb 转 16进制 + * @param rgb RGB + * @returns #HEX{6} + */ +function rgbToHex(rgb: RGB): string { + const HEX_MAP_REVERSE: Record = {}; + for (const key in HEX_MAP) { + HEX_MAP_REVERSE[HEX_MAP[key as HEX]] = key as HEX; + } + function getRemainderAndQuotient(val: number): string { + val = Math.round(val); + return `${HEX_MAP_REVERSE[Math.floor(val / 16)]}${ + HEX_MAP_REVERSE[val % 16] + }`; + } + + return `#${getRemainderAndQuotient(rgb.r)}${getRemainderAndQuotient( + rgb.g + )}${getRemainderAndQuotient(rgb.b)}`; +} + +// hsl 转 16进制 +function hslToHex(hsl: HSL): string { + return rgbToHex(hslToRgb(hsl)); +} + +// 16进制 转 hsl +function hexToHsl(hex: string): HSL { + return rgbToHsl(hexToRGB(hex)); +} + +// 生成混合色(混黑 + 混白) +function genMixColor(base: string | RGB | HSL): { + DEFAULT: string; + dark: { + 1: string; + 2: string; + 3: string; + 4: string; + 5: string; + 6: string; + 7: string; + 8: string; + 9: string; + }; + light: { + 1: string; + 2: string; + 3: string; + 4: string; + 5: string; + 6: string; + 7: string; + 8: string; + 9: string; + }; +} { + // 基准色统一转换为RGB + if (typeof base === "string") { + base = hexToRGB(base); + } else if ("h" in base) { + base = hslToRgb(base); + } + + // 混合色 + function mix(color: RGB, mixColor: RGB, weight: number): RGB { + return { + r: color.r * (1 - weight) + mixColor.r * weight, + g: color.g * (1 - weight) + mixColor.g * weight, + b: color.b * (1 - weight) + mixColor.b * weight, + }; + } + + return { + DEFAULT: rgbToHex(base), + dark: { + 1: rgbToHex(mix(base, rgbBlack, 0.1)), + 2: rgbToHex(mix(base, rgbBlack, 0.2)), + 3: rgbToHex(mix(base, rgbBlack, 0.3)), + 4: rgbToHex(mix(base, rgbBlack, 0.4)), + 5: rgbToHex(mix(base, rgbBlack, 0.5)), + 6: rgbToHex(mix(base, rgbBlack, 0.6)), + 7: rgbToHex(mix(base, rgbBlack, 0.7)), + 8: rgbToHex(mix(base, rgbBlack, 0.78)), + 9: rgbToHex(mix(base, rgbBlack, 0.85)), + }, + light: { + 1: rgbToHex(mix(base, rgbWhite, 0.1)), + 2: rgbToHex(mix(base, rgbWhite, 0.2)), + 3: rgbToHex(mix(base, rgbWhite, 0.3)), + 4: rgbToHex(mix(base, rgbWhite, 0.4)), + 5: rgbToHex(mix(base, rgbWhite, 0.5)), + 6: rgbToHex(mix(base, rgbWhite, 0.6)), + 7: rgbToHex(mix(base, rgbWhite, 0.7)), + 8: rgbToHex(mix(base, rgbWhite, 0.78)), + 9: rgbToHex(mix(base, rgbWhite, 0.85)), + }, + }; +} + +export { + genMixColor, + rgbToHsl, + rgbToHex, + hslToRgb, + hslToHex, + hexToRGB, + hexToHsl, +}; diff --git a/src/utils/theme.ts b/src/utils/theme.ts deleted file mode 100644 index edb88c6..0000000 --- a/src/utils/theme.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * hex颜色转rgb颜色 - * @param str 颜色值字符串 - * @return 返回处理后的颜色值 - */ -export function hexToRgb(str: any) { - let hexs: any = ""; - const reg = /^\#?[0-9A-Fa-f]{6}$/; - if (!reg.test(str)) return ElMessage.warning("错误的hex"); - str = str.replace("#", ""); - hexs = str.match(/../g); - for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16); - return hexs; -} - -/** - * rgb颜色转Hex颜色 - * @param r 代表红色 - * @param g 代表绿色 - * @param b 代表蓝色 - * @return 返回处理后的颜色值 - */ -export function rgbToHex(r: any, g: any, b: any) { - const reg = /^\d{1,3}$/; - if (!reg.test(r) || !reg.test(g) || !reg.test(b)) - return ElMessage.warning("错误的rgb颜色值"); - const hexs = [r.toString(16), g.toString(16), b.toString(16)]; - for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`; - return `#${hexs.join("")}`; -} - -/** - * 加深颜色值 - * @param color 颜色值字符串 - * @param level 加深的程度,限0-1之间 - * @return 返回处理后的颜色值 - */ -export function getDarkColor(color: string, level: number) { - const reg = /^\#?[0-9A-Fa-f]{6}$/; - if (!reg.test(color)) return ElMessage.warning("错误的hex颜色值"); - const rgb = hexToRgb(color); - for (let i = 0; i < 3; i++) - rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level)); - return rgbToHex(rgb[0], rgb[1], rgb[2]); -} - -/** - * 变浅颜色值 - * @param color 颜色值字符串 - * @param level 加深的程度,限0-1之间 - * @return 返回处理后的颜色值 - */ -export function getLightColor(color: string, level: number) { - const reg = /^\#?[0-9A-Fa-f]{6}$/; - if (!reg.test(color)) return ElMessage.warning("错误的hex颜色值"); - const rgb = hexToRgb(color); - for (let i = 0; i < 3; i++) - rgb[i] = Math.round(255 * level + rgb[i] * (1 - level)); - return rgbToHex(rgb[0], rgb[1], rgb[2]); -} - -/** - * 根据dark加工颜色值 - * @param color 颜色值字符串 - * @param level 加深的程度,限0-1之间 - * @return 返回处理后的颜色值 - */ -export function getThemeColor(dark: boolean, color: string, level: number) { - return dark ? getDarkColor(color, level) : getLightColor(color, level); -}