feat: 添加整体风格自适应操作系统浅色、深色、自动主题功能,让平台充满现代气息 (#869)

* feat: 添加整体风格自适应操作系统浅色、深色、自动主题功能,让平台更加现代化

* chore: update
This commit is contained in:
xiaoming 2024-01-14 23:21:43 +08:00 committed by GitHub
parent 61c6ec230a
commit dd78313622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 100 additions and 30 deletions

View File

@ -47,7 +47,7 @@
<img alt="PureAdmin" src="https://xiaoxian521.github.io/hyperlink/img/vue-pure-admin/2.jpg"> <img alt="PureAdmin" src="https://xiaoxian521.github.io/hyperlink/img/vue-pure-admin/2.jpg">
</p> </p>
黑模式 色风格
<p align="center"> <p align="center">
<img alt="PureAdmin" src="https://xiaoxian521.github.io/hyperlink/img/vue-pure-admin/3.jpg"> <img alt="PureAdmin" src="https://xiaoxian521.github.io/hyperlink/img/vue-pure-admin/3.jpg">

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.11 323.11 0 0 1-107.769-242.852z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

Before

Width:  |  Height:  |  Size: 708 B

After

Width:  |  Height:  |  Size: 706 B

View File

@ -9,6 +9,7 @@
"Layout": "vertical", "Layout": "vertical",
"Theme": "light", "Theme": "light",
"DarkMode": false, "DarkMode": false,
"OverallStyle": "light",
"Grey": false, "Grey": false,
"Weak": false, "Weak": false,
"HideTabs": false, "HideTabs": false,

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.11 323.11 0 0 1-107.769-242.852z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

Before

Width:  |  Height:  |  Size: 708 B

After

Width:  |  Height:  |  Size: 706 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="icon" viewBox="0 0 1024 1024"><path d="M554 849.574c0 23.365-18.635 42.307-42 42.307s-42-18.941-42-42.307V662.719c0-23.365 18.635-42.307 42-42.307v-7.051c23.365 0 42 25.993 42 49.358z"/><path d="M893 888.5c0 17.397-14.103 31.5-31.5 31.5h-700c-17.397 0-31.5-14.103-31.5-31.5s14.103-31.5 31.5-31.5h700c17.397 0 31.5 14.103 31.5 31.5m33-714.074C926 135.484 894.686 105 855.744 105H168.256C129.314 105 98 135.484 98 174.426V533h828zM98 630.988C98 669.931 129.314 702 168.256 702h687.488C894.686 702 926 669.931 926 630.988V596H98z"/></svg>

After

Width:  |  Height:  |  Size: 605 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -115,7 +115,13 @@ export default defineComponent({
onClick={event => handleChange({ option, index }, event)} onClick={event => handleChange({ option, index }, event)}
> >
<input type="radio" name="segmented" /> <input type="radio" name="segmented" />
<div class="pure-segmented-item-label"> <div
class="pure-segmented-item-label"
v-tippy={{
content: option?.tip,
zIndex: 41000
}}
>
{option.icon && !isFunction(option.label) ? ( {option.icon && !isFunction(option.label) ? (
<span <span
class="pure-segmented-item-icon" class="pure-segmented-item-icon"

View File

@ -15,4 +15,6 @@ export interface OptionsType {
value?: string | number; value?: string | number;
/** 是否禁用 */ /** 是否禁用 */
disabled?: boolean; disabled?: boolean;
/** `tooltip` 提示 */
tip?: string;
} }

View File

@ -6,6 +6,7 @@ import {
reactive, reactive,
computed, computed,
nextTick, nextTick,
onUnmounted,
onBeforeMount onBeforeMount
} from "vue"; } from "vue";
import panel from "../panel/index.vue"; import panel from "../panel/index.vue";
@ -21,6 +22,7 @@ import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import Check from "@iconify-icons/ep/check"; import Check from "@iconify-icons/ep/check";
import dayIcon from "@/assets/svg/day.svg?component"; import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component"; import darkIcon from "@/assets/svg/dark.svg?component";
import systemIcon from "@/assets/svg/system.svg?component";
const { device } = useNav(); const { device } = useNav();
const { isDark } = useDark(); const { isDark } = useDark();
@ -32,6 +34,7 @@ const horizontalRef = ref();
const { const {
dataTheme, dataTheme,
overallStyle,
layoutTheme, layoutTheme,
themeColors, themeColors,
toggleClass, toggleClass,
@ -70,7 +73,7 @@ const getThemeColorStyle = computed(() => {
}; };
}); });
/** 当网页为暗黑模式时不显示亮白色切换选项 */ /** 当网页整体为暗色风格时不显示亮白色主题配色切换选项 */
const showThemeColors = computed(() => { const showThemeColors = computed(() => {
return themeColor => { return themeColor => {
return themeColor === "light" && isDark.value ? false : true; return themeColor === "light" && isDark.value ? false : true;
@ -162,13 +165,24 @@ const getThemeColor = computed(() => {
const themeOptions = computed<Array<OptionsType>>(() => { const themeOptions = computed<Array<OptionsType>>(() => {
return [ return [
{ {
label: "色", label: "色",
icon: dayIcon, icon: dayIcon,
theme: "light",
tip: "清新启航,点亮舒适的工作界面",
iconAttrs: { fill: isDark.value ? "#fff" : "#000" } iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
}, },
{ {
label: "色", label: "色",
icon: darkIcon, icon: darkIcon,
theme: "dark",
tip: "月光序曲,沉醉于夜的静谧雅致",
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
},
{
label: "自动",
icon: systemIcon,
theme: "system",
tip: "同步时光,界面随晨昏自然呼应",
iconAttrs: { fill: isDark.value ? "#fff" : "#000" } iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
} }
]; ];
@ -177,10 +191,12 @@ const themeOptions = computed<Array<OptionsType>>(() => {
const markOptions: Array<OptionsType> = [ const markOptions: Array<OptionsType> = [
{ {
label: "灵动", label: "灵动",
tip: "灵动标签,添趣生辉",
value: "smart" value: "smart"
}, },
{ {
label: "卡片", label: "卡片",
tip: "卡片标签,高效浏览",
value: "card" value: "card"
} }
]; ];
@ -195,7 +211,8 @@ function setLayoutModel(layout: string) {
darkMode: $storage.layout?.darkMode, darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus, sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor, epThemeColor: $storage.layout?.epThemeColor,
themeColor: layoutTheme.value.theme themeColor: $storage.layout?.themeColor,
overallStyle: $storage.layout?.overallStyle
}; };
useAppStoreHook().setLayout(layout); useAppStoreHook().setLayout(layout);
} }
@ -220,9 +237,34 @@ watch($storage, ({ layout }) => {
} }
}); });
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
/** 根据操作系统主题设置平台整体风格 */
function updateTheme() {
if (overallStyle.value !== "system") return;
if (mediaQueryList.matches) {
dataTheme.value = true;
} else {
dataTheme.value = false;
}
dataThemeChange(overallStyle.value);
}
function removeMatchMedia() {
mediaQueryList.removeEventListener("change", updateTheme);
}
/** 监听操作系统主题改变 */
function watchSystemThemeChange() {
updateTheme();
removeMatchMedia();
mediaQueryList.addEventListener("change", updateTheme);
}
onBeforeMount(() => { onBeforeMount(() => {
/* 初始化项目配置 */ /* 初始化项目配置 */
nextTick(() => { nextTick(() => {
watchSystemThemeChange();
settings.greyVal && settings.greyVal &&
document.querySelector("html")?.setAttribute("class", "html-grey"); document.querySelector("html")?.setAttribute("class", "html-grey");
settings.weakVal && settings.weakVal &&
@ -231,6 +273,8 @@ onBeforeMount(() => {
settings.hideFooter && hideFooterChange(); settings.hideFooter && hideFooterChange();
}); });
}); });
onUnmounted(() => removeMatchMedia);
</script> </script>
<template> <template>
@ -238,12 +282,17 @@ onBeforeMount(() => {
<div class="p-6"> <div class="p-6">
<p class="mb-3 font-medium text-sm dark:text-white">整体风格</p> <p class="mb-3 font-medium text-sm dark:text-white">整体风格</p>
<Segmented <Segmented
:modelValue="dataTheme ? 1 : 0" class="select-none"
:modelValue="overallStyle === 'system' ? 2 : dataTheme ? 1 : 0"
:options="themeOptions" :options="themeOptions"
@change=" @change="
{ theme => {
dataTheme = !dataTheme; theme.index === 1 && theme.index !== 2
dataThemeChange(); ? (dataTheme = true)
: (dataTheme = false);
overallStyle = theme.option.theme;
dataThemeChange(theme.option.theme);
theme.index === 2 && watchSystemThemeChange();
} }
" "
/> />
@ -272,7 +321,7 @@ onBeforeMount(() => {
<li <li
ref="verticalRef" ref="verticalRef"
v-tippy="{ v-tippy="{
content: '左侧菜单', content: '左侧菜单,亲切熟悉',
zIndex: 41000 zIndex: 41000
}" }"
:class="layoutTheme.layout === 'vertical' ? 'is-select' : ''" :class="layoutTheme.layout === 'vertical' ? 'is-select' : ''"
@ -285,7 +334,7 @@ onBeforeMount(() => {
v-if="device !== 'mobile'" v-if="device !== 'mobile'"
ref="horizontalRef" ref="horizontalRef"
v-tippy="{ v-tippy="{
content: '顶部菜单', content: '顶部菜单,简洁概览',
zIndex: 41000 zIndex: 41000
}" }"
:class="layoutTheme.layout === 'horizontal' ? 'is-select' : ''" :class="layoutTheme.layout === 'horizontal' ? 'is-select' : ''"
@ -298,7 +347,7 @@ onBeforeMount(() => {
v-if="device !== 'mobile'" v-if="device !== 'mobile'"
ref="mixRef" ref="mixRef"
v-tippy="{ v-tippy="{
content: '混合菜单', content: '混合菜单,灵活多变',
zIndex: 41000 zIndex: 41000
}" }"
:class="layoutTheme.layout === 'mix' ? 'is-select' : ''" :class="layoutTheme.layout === 'mix' ? 'is-select' : ''"
@ -311,6 +360,7 @@ onBeforeMount(() => {
<p class="mt-5 mb-3 font-medium text-base dark:text-white">页签风格</p> <p class="mt-5 mb-3 font-medium text-base dark:text-white">页签风格</p>
<Segmented <Segmented
class="select-none"
:modelValue="markValue === 'smart' ? 0 : 1" :modelValue="markValue === 'smart' ? 0 : 1"
:options="markOptions" :options="markOptions"
@change="onChange" @change="onChange"

View File

@ -38,6 +38,7 @@ export function useDataThemeChange() {
const { $storage } = useGlobal<GlobalPropertiesApi>(); const { $storage } = useGlobal<GlobalPropertiesApi>();
const dataTheme = ref<boolean>($storage?.layout?.darkMode); const dataTheme = ref<boolean>($storage?.layout?.darkMode);
const overallStyle = ref<string>($storage?.layout?.overallStyle);
const body = document.documentElement as HTMLElement; const body = document.documentElement as HTMLElement;
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) { function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
@ -64,7 +65,8 @@ export function useDataThemeChange() {
darkMode: dataTheme.value, darkMode: dataTheme.value,
sidebarStatus: $storage.layout?.sidebarStatus, sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor, epThemeColor: $storage.layout?.epThemeColor,
themeColor: isClick ? theme : storageThemeColor themeColor: isClick ? theme : storageThemeColor,
overallStyle: overallStyle.value
}; };
if (theme === "default" || theme === "light") { if (theme === "default" || theme === "light") {
@ -94,8 +96,9 @@ export function useDataThemeChange() {
} }
}; };
/** 亮色、暗色整体风格切换 */ /** 浅色、深色整体风格切换 */
function dataThemeChange() { function dataThemeChange(overall?: string) {
overallStyle.value = overall;
if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) { if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
setLayoutThemeColor("default", false); setLayoutThemeColor("default", false);
} else { } else {
@ -130,6 +133,7 @@ export function useDataThemeChange() {
return { return {
body, body,
dataTheme, dataTheme,
overallStyle,
layoutTheme, layoutTheme,
themeColors, themeColors,
onReset, onReset,

View File

@ -28,7 +28,8 @@ export function useLayout() {
darkMode: $config?.DarkMode ?? false, darkMode: $config?.DarkMode ?? false,
sidebarStatus: $config?.SidebarStatus ?? true, sidebarStatus: $config?.SidebarStatus ?? true,
epThemeColor: $config?.EpThemeColor ?? "#409EFF", epThemeColor: $config?.EpThemeColor ?? "#409EFF",
themeColor: $config?.Theme ?? "light" themeColor: $config?.Theme ?? "light",
overallStyle: $config?.OverallStyle ?? "light"
}; };
} }
/** 灰色模式、色弱模式、隐藏标签页 */ /** 灰色模式、色弱模式、隐藏标签页 */

View File

@ -73,7 +73,8 @@ function setTheme(layoutModel: string) {
darkMode: $storage.layout?.darkMode, darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus, sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor, epThemeColor: $storage.layout?.epThemeColor,
themeColor: $storage.layout?.themeColor themeColor: $storage.layout?.themeColor,
overallStyle: $storage.layout?.overallStyle
}; };
} }
@ -119,7 +120,7 @@ onMounted(() => {
}); });
onBeforeMount(() => { onBeforeMount(() => {
useDataThemeChange().dataThemeChange(); useDataThemeChange().dataThemeChange($storage.layout?.overallStyle);
}); });
const layoutHeader = defineComponent({ const layoutHeader = defineComponent({

View File

@ -1,6 +1,6 @@
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *; @use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
/* 暗黑模式适配 */ /* 整体暗色风格适配 */
html.dark { html.dark {
$border-style: #303030; $border-style: #303030;
$color-white: #fff; $color-white: #fff;
@ -126,7 +126,7 @@ html.dark {
} }
} }
/* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,非暗黑模式在 src/style/element-plus.scss 文件进行了适配 */ /* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,整体浅色风格在 src/style/element-plus.scss 文件进行了适配 */
.pure-message { .pure-message {
background-color: rgb(36 37 37) !important; background-color: rgb(36 37 37) !important;
background-image: initial !important; background-image: initial !important;

View File

@ -116,7 +116,7 @@
} }
} }
/* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,暗黑模式在 src/style/dark.scss 文件进行了适配 */ /* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,整体暗色风格在 src/style/dark.scss 文件进行了适配 */
.pure-message { .pure-message {
padding: 10px 13px !important; padding: 10px 13px !important;
background: #fff !important; background: #fff !important;
@ -189,7 +189,7 @@
} }
} }
/* 仿 el-scrollbar 滚动条样式支持大多数浏览器如Chrome、Edge、Firefox、Safari等。暗黑模式在 src/style/dark.scss 文件进行了适配 */ /* 仿 el-scrollbar 滚动条样式支持大多数浏览器如Chrome、Edge、Firefox、Safari等。整体暗色风格在 src/style/dark.scss 文件进行了适配 */
.pure-scrollbar { .pure-scrollbar {
/* Firefox */ /* Firefox */
scrollbar-width: thin; /* 可选值为 'auto', 'thin', 'none' */ scrollbar-width: thin; /* 可选值为 'auto', 'thin', 'none' */

View File

@ -19,7 +19,8 @@ export const injectResponsiveStorage = (app: App, config: PlatformConfigs) => {
darkMode: config.DarkMode ?? false, darkMode: config.DarkMode ?? false,
sidebarStatus: config.SidebarStatus ?? true, sidebarStatus: config.SidebarStatus ?? true,
epThemeColor: config.EpThemeColor ?? "#409EFF", epThemeColor: config.EpThemeColor ?? "#409EFF",
themeColor: config.Theme ?? "light" // 主题色对应项目配置中的主题色与theme不同的是它不会受到亮色、暗色整体风格切换的影响只会在手动点击主题色时改变 themeColor: config.Theme ?? "light", // 主题色对应项目配置中的主题色与theme不同的是它不会受到浅色、深色整体风格切换的影响只会在手动点击主题色时改变
overallStyle: config.OverallStyle ?? "light" // 整体风格浅色light、深色dark、自动system
}, },
// 项目配置-界面显示 // 项目配置-界面显示
configure: Storage.getData("configure", nameSpace) ?? { configure: Storage.getData("configure", nameSpace) ?? {

View File

@ -117,7 +117,7 @@ defineOptions({
<h4 class="mb-4"> <h4 class="mb-4">
类似 Ant Design 风格的消息提示点击弹出提示信息基于 ElMessage 类似 Ant Design 风格的消息提示点击弹出提示信息基于 ElMessage
样式改版不会影响 ElMessage 样式改版不会影响 ElMessage
原本样式使用和打包大小成本极低并适配暗黑模式 原本样式使用和打包大小成本极低并适配整体暗色风格
</h4> </h4>
<el-space wrap> <el-space wrap>

View File

@ -56,8 +56,8 @@ const currentPage = computed(() => {
const { t } = useI18n(); const { t } = useI18n();
const { initStorage } = useLayout(); const { initStorage } = useLayout();
initStorage(); initStorage();
const { dataTheme, dataThemeChange } = useDataThemeChange(); const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
dataThemeChange(); dataThemeChange(overallStyle.value);
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav(); const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
const { locale, translationCh, translationEn } = useTranslationLang(); const { locale, translationCh, translationEn } = useTranslationLang();

3
types/global.d.ts vendored
View File

@ -83,6 +83,7 @@ declare global {
Layout?: string; Layout?: string;
Theme?: string; Theme?: string;
DarkMode?: boolean; DarkMode?: boolean;
OverallStyle?: string;
Grey?: boolean; Grey?: boolean;
Weak?: boolean; Weak?: boolean;
HideTabs?: boolean; HideTabs?: boolean;
@ -127,6 +128,7 @@ declare global {
sidebarStatus?: boolean; sidebarStatus?: boolean;
epThemeColor?: string; epThemeColor?: string;
themeColor?: string; themeColor?: string;
overallStyle?: string;
showLogo?: boolean; showLogo?: boolean;
showModel?: string; showModel?: string;
mapConfigure?: { mapConfigure?: {
@ -154,6 +156,7 @@ declare global {
sidebarStatus?: boolean; sidebarStatus?: boolean;
epThemeColor?: string; epThemeColor?: string;
themeColor?: string; themeColor?: string;
overallStyle?: string;
}; };
configure: { configure: {
grey?: boolean; grey?: boolean;