Browse Source

feat(Layout): Add classic layout

陈凯龙 3 years ago
parent
commit
839b6015b8

+ 2 - 1
.vscode/settings.json

@@ -14,5 +14,6 @@
   "i18n-ally.enabledParsers": ["ts"],
   "i18n-ally.sourceLanguage": "en",
   "i18n-ally.displayLanguage": "zh-CN",
-  "i18n-ally.enabledFrameworks": ["vue", "react"]
+  "i18n-ally.enabledFrameworks": ["vue", "react"],
+  "god.tsconfig": "./tsconfig.json"
 }

+ 16 - 3
src/App.vue

@@ -6,7 +6,9 @@ import { isDark } from '@/utils/is'
 
 const appStore = useAppStore()
 
-const size = computed(() => appStore.size)
+const currentSize = computed(() => appStore.getCurrentSize)
+
+const greyMode = computed(() => appStore.getGreyMode)
 
 const initDark = () => {
   const isDarkTheme = isDark()
@@ -17,12 +19,14 @@ initDark()
 </script>
 
 <template>
-  <ConfigGlobal :size="size">
-    <RouterView />
+  <ConfigGlobal :size="currentSize">
+    <RouterView :class="{ 'v-grey__mode': greyMode }" />
   </ConfigGlobal>
 </template>
 
 <style lang="less">
+@prefix-cls: ~'@{namespace}-grey';
+
 .size {
   width: 100%;
   height: 100%;
@@ -39,4 +43,13 @@ body {
     .size;
   }
 }
+
+.@{prefix-cls}__mode {
+  -webkit-filter: grayscale(100%);
+  -moz-filter: grayscale(100%);
+  -ms-filter: grayscale(100%);
+  -o-filter: grayscale(100%);
+  filter: grayscale(100%);
+  filter: progid:dximagetransform.microsoft.basicimage(grayscale=1);
+}
 </style>

+ 7 - 2
src/components/Breadcrumb/src/Breadcrumb.vue

@@ -2,13 +2,18 @@
 import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
 import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
 import { useRouter } from 'vue-router'
-// import { compile } from 'path-to-regexp'
 import { usePermissionStore } from '@/store/modules/permission'
 import { filterBreadcrumb } from './helper'
 import { filter, treeToList } from '@/utils/tree'
 import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
 import { useI18n } from '@/hooks/web/useI18n'
 import { Icon } from '@/components/Icon'
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
+
+// 面包屑图标
+const breadcrumbIcon = computed(() => appStore.getBreadcrumbIcon)
 
 export default defineComponent({
   name: 'Breadcrumb',
@@ -41,7 +46,7 @@ export default defineComponent({
         const meta = v.meta as RouteMeta
         return (
           <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
-            {meta?.icon ? (
+            {meta?.icon && breadcrumbIcon.value ? (
               <>
                 <Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title)}
               </>

+ 3 - 0
src/components/ColorRadioPicker/index.ts

@@ -0,0 +1,3 @@
+import ColorRadioPicker from './src/ColorRadioPicker.vue'
+
+export { ColorRadioPicker }

+ 60 - 0
src/components/ColorRadioPicker/src/ColorRadioPicker.vue

@@ -0,0 +1,60 @@
+<script setup lang="ts">
+import { PropType, watch, unref, ref } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+const props = defineProps({
+  schema: {
+    type: Array as PropType<string[]>,
+    default: () => []
+  },
+  modelValue: propTypes.string.def('')
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const colorVal = ref(props.modelValue)
+
+watch(
+  () => props.modelValue,
+  (val: string) => {
+    if (val === unref(colorVal)) return
+    colorVal.value = val
+  }
+)
+
+// 监听
+watch(
+  () => colorVal.value,
+  (val: string) => {
+    emit('update:modelValue', val)
+    emit('change', val)
+  }
+)
+</script>
+
+<template>
+  <div class="v-color-radio-picker flex flex-wrap space-x-14px">
+    <span
+      v-for="(item, i) in schema"
+      :key="`radio-${i}`"
+      class="v-color-radio-picker w-20px h-20px cursor-pointer rounded-2px border-solid border-gray-300 border-2px text-center leading-20px mb-5px"
+      :class="{ 'is-active': colorVal === item }"
+      :style="{
+        background: item
+      }"
+      @click="colorVal = item"
+    >
+      <Icon v-if="colorVal === item" color="#fff" icon="ep:check" :size="16" />
+    </span>
+  </div>
+</template>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-color-radio-picker';
+
+.@{prefix-cls} {
+  .is-active {
+    border-color: var(--el-color-primary);
+  }
+}
+</style>

+ 16 - 9
src/components/ConfigGlobal/src/ConfigGlobal.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { provide, computed, watch } from 'vue'
+import { provide, computed, watch, onMounted } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { ElConfigProvider } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
@@ -9,8 +9,20 @@ import { setCssVar } from '@/utils'
 
 const appStore = useAppStore()
 
+const props = defineProps({
+  size: propTypes.oneOf<ElememtPlusSzie[]>(['default', 'small', 'large']).def('default')
+})
+
+provide('configGlobal', props)
+
+// 初始化所有主题色
+onMounted(() => {
+  appStore.setCssVarTheme()
+})
+
 const { width } = useWindowSize()
 
+// 监听窗口变化
 watch(
   () => width.value,
   (width: number) => {
@@ -29,19 +41,14 @@ watch(
   }
 )
 
+// 多语言相关
 const localeStore = useLocaleStore()
 
-const locale = computed(() => localeStore.locale)
-
-const props = defineProps({
-  size: propTypes.oneOf<ElememtPlusSzie[]>(['default', 'small', 'large']).def('default')
-})
-
-provide('configGlobal', props)
+const currentLocale = computed(() => localeStore.currentLocale)
 </script>
 
 <template>
-  <ElConfigProvider :locale="locale.elLocale" :message="{ max: 1 }" :size="size">
+  <ElConfigProvider :locale="currentLocale.elLocale" :message="{ max: 1 }" :size="size">
     <slot></slot>
   </ElConfigProvider>
 </template>

+ 2 - 2
src/components/LocaleDropdown/src/LocaleDropdown.vue

@@ -8,13 +8,13 @@ const localeStore = useLocaleStore()
 
 const langMap = computed(() => localeStore.getLocaleMap)
 
-const currentLang = computed(() => localeStore.getLocale)
+const currentLang = computed(() => localeStore.getCurrentLocale)
 
 const setLang = (lang: LocaleType) => {
   if (lang === unref(currentLang).lang) return
   // 需要重新加载页面让整个语言多初始化
   window.location.reload()
-  localeStore.setLocale({
+  localeStore.setCurrentLocale({
     lang
   })
   const { changeLocale } = useLocale()

+ 21 - 2
src/components/Logo/src/Logo.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, watch, computed } from 'vue'
+import { ref, watch, computed, onMounted, unref } from 'vue'
 import { useAppStore } from '@/store/modules/app'
 
 const appStore = useAppStore()
@@ -12,6 +12,10 @@ const layout = computed(() => appStore.getLayout)
 
 const collapse = computed(() => appStore.getCollapse)
 
+onMounted(() => {
+  if (unref(collapse)) show.value = false
+})
+
 watch(
   () => collapse.value,
   (collapse: boolean) => {
@@ -37,7 +41,7 @@ watch(
       {
         'v-logo__Top': layout !== 'classic'
       },
-      'flex h-[var(--logo-height)] items-center cursor-pointer pl-8px'
+      'flex h-[var(--logo-height)] items-center cursor-pointer pl-8px relative'
     ]"
     to="/"
   >
@@ -50,3 +54,18 @@ watch(
     }}</div>
   </router-link>
 </template>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-logo';
+
+.@{prefix-cls} {
+  &:after {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 100%;
+    border-bottom: 1px solid var(--logo-border-color);
+    content: '';
+  }
+}
+</style>

+ 52 - 2
src/components/Menu/src/Menu.vue

@@ -14,6 +14,9 @@ export default defineComponent({
   setup() {
     const appStore = useAppStore()
 
+    // logo
+    const logo = computed(() => appStore.logo)
+
     const { push, currentRoute } = useRouter()
 
     const permissionStore = usePermissionStore()
@@ -61,8 +64,8 @@ export default defineComponent({
           'bg-[var(--left-menu-bg-color)]'
         ]}
       >
-        <Logo></Logo>
-        <ElScrollbar class={[{ '!h-[calc(100%-var(--top-tool-height))]': true }]}>
+        {logo.value ? <Logo></Logo> : undefined}
+        <ElScrollbar class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}>
           <ElMenu
             defaultActive={unref(activeMenu)}
             mode={unref(menuMode)}
@@ -89,9 +92,28 @@ export default defineComponent({
 <style lang="less" scoped>
 @prefix-cls: ~'@{namespace}-menu';
 
+.is-active--after {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 4px;
+  height: 100%;
+  background-color: var(--el-color-primary);
+  content: '';
+}
+
 .@{prefix-cls} {
   transition: width var(--transition-time-02);
 
+  &:after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    height: 100%;
+    border-left: 1px solid var(--left-menu-border-color);
+    content: '';
+  }
+
   :deep(.el-menu) {
     width: 100% !important;
     border-right: none;
@@ -123,6 +145,14 @@ export default defineComponent({
       }
     }
 
+    .el-menu-item.is-active {
+      position: relative;
+
+      &:after {
+        .is-active--after;
+      }
+    }
+
     // 设置子菜单的背景颜色
     .el-menu {
       .el-sub-menu__title,
@@ -138,7 +168,12 @@ export default defineComponent({
 
     & > .is-active,
     & > .is-active > .el-sub-menu__title {
+      position: relative;
       background-color: var(--left-menu-collapse-bg-active-color) !important;
+
+      &:after {
+        .is-active--after;
+      }
     }
   }
 
@@ -155,6 +190,16 @@ export default defineComponent({
 <style lang="less">
 @prefix-cls: ~'@{namespace}-menu-popper';
 
+.is-active--after {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 4px;
+  height: 100%;
+  background-color: var(--el-color-primary);
+  content: '';
+}
+
 .@{prefix-cls} {
   &--vertical {
     // 设置选中时子标题的颜色
@@ -175,11 +220,16 @@ export default defineComponent({
 
     // 设置选中时的高亮背景
     .el-menu-item.is-active {
+      position: relative;
       background-color: var(--left-menu-bg-active-color) !important;
 
       &:hover {
         background-color: var(--left-menu-bg-active-color) !important;
       }
+
+      &:after {
+        .is-active--after;
+      }
     }
   }
 }

+ 110 - 3
src/components/Setting/src/Setting.vue

@@ -1,8 +1,64 @@
 <script setup lang="ts">
-import { ElDrawer } from 'element-plus'
-import { ref } from 'vue'
+import { ElDrawer, ElDivider } from 'element-plus'
+import { ref, unref } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ThemeSwitch } from '@/components/ThemeSwitch'
+import { ColorRadioPicker } from '@/components/ColorRadioPicker'
+import { colorIsDark, lighten, hexToRGB } from '@/utils/color'
+import { useCssVar } from '@vueuse/core'
+import { useAppStore } from '@/store/modules/app'
+import { trim, setCssVar } from '@/utils'
+import InterfaceDisplay from './components/InterfaceDisplay.vue'
+
+const appStore = useAppStore()
+
+const { t } = useI18n()
 
 const drawer = ref(false)
+
+// 主题色相关
+const systemTheme = ref(appStore.getTheme.elColorPrimary)
+
+const setSystemTheme = (color: string) => {
+  setCssVar('--el-color-primary', color)
+  appStore.setTheme({ elColorPrimary: color })
+  const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
+  setMenuTheme(trim(unref(leftMenuBgColor)))
+}
+
+// 菜单主题相关
+const menuTheme = ref(appStore.getTheme.leftMenuBgColor)
+
+const setMenuTheme = (color: string) => {
+  const primaryColor = useCssVar('--el-color-primary', document.documentElement)
+  const isDarkColor = colorIsDark(color)
+  const theme: Recordable = {
+    // 左侧菜单边框颜色
+    leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
+    // 左侧菜单背景颜色
+    leftMenuBgColor: color,
+    // 左侧菜单浅色背景颜色
+    leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
+    // 左侧菜单选中背景颜色
+    leftMenuBgActiveColor: isDarkColor
+      ? 'var(--el-color-primary)'
+      : hexToRGB(unref(primaryColor), 0.1),
+    // 左侧菜单收起选中背景颜色
+    leftMenuCollapseBgActiveColor: isDarkColor
+      ? 'var(--el-color-primary)'
+      : hexToRGB(unref(primaryColor), 0.1),
+    // 左侧菜单字体颜色
+    leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
+    // 左侧菜单选中字体颜色
+    leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
+    // logo字体颜色
+    logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
+    // logo边框颜色
+    logoBorderColor: isDarkColor ? 'inherit' : '#eee'
+  }
+  appStore.setTheme(theme)
+  appStore.setCssVarTheme()
+}
 </script>
 
 <template>
@@ -13,7 +69,58 @@ const drawer = ref(false)
     <Icon icon="ant-design:setting-outlined" color="#fff" />
   </div>
 
-  <ElDrawer v-model="drawer" :with-header="false" direction="rtl" size="300px">ddd</ElDrawer>
+  <ElDrawer v-model="drawer" direction="rtl" size="350px">
+    <template #title>
+      <span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
+    </template>
+
+    <div class="text-center">
+      <!-- 主题 -->
+      <ElDivider>{{ t('setting.theme') }}</ElDivider>
+      <ThemeSwitch />
+
+      <!-- 布局 -->
+      <ElDivider>{{ t('setting.layout') }}</ElDivider>
+
+      <!-- 系统主题 -->
+      <ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
+      <ColorRadioPicker
+        v-model="systemTheme"
+        :schema="[
+          '#409eff',
+          '#009688',
+          '#536dfe',
+          '#ff5c93',
+          '#ee4f12',
+          '#0096c7',
+          '#9c27b0',
+          '#ff9800'
+        ]"
+        @change="setSystemTheme"
+      />
+
+      <!-- 菜单主题 -->
+      <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
+      <ColorRadioPicker
+        v-model="menuTheme"
+        :schema="[
+          '#fff',
+          '#001529',
+          '#212121',
+          '#273352',
+          '#191b24',
+          '#383f45',
+          '#001628',
+          '#344058'
+        ]"
+        @change="setMenuTheme"
+      />
+    </div>
+
+    <!-- 界面显示 -->
+    <ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
+    <InterfaceDisplay />
+  </ElDrawer>
 </template>
 
 <style lang="less" scoped>

+ 134 - 0
src/components/Setting/src/components/InterfaceDisplay.vue

@@ -0,0 +1,134 @@
+<script setup lang="ts">
+import { ElSwitch } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useAppStore } from '@/store/modules/app'
+import { ref } from 'vue'
+
+const appStore = useAppStore()
+
+const { t } = useI18n()
+
+// 面包屑
+const breadcrumb = ref(appStore.getBreadcrumb)
+
+const breadcrumbChange = (show: boolean) => {
+  appStore.setBreadcrumb(show)
+}
+
+// 面包屑图标
+const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
+
+const breadcrumbIconChange = (show: boolean) => {
+  appStore.setBreadcrumbIcon(show)
+}
+
+// 折叠菜单
+const collapse = ref(appStore.getCollapse)
+
+const collapseChange = (show: boolean) => {
+  appStore.setCollapse(show)
+}
+
+// 折叠图标
+const hamburger = ref(appStore.getHamburger)
+
+const hamburgerChange = (show: boolean) => {
+  appStore.setHamburger(show)
+}
+
+// 全屏图标
+const screenfull = ref(appStore.getScreenfull)
+
+const screenfullChange = (show: boolean) => {
+  appStore.setScreenfull(show)
+}
+
+// 尺寸图标
+const size = ref(appStore.getSize)
+
+const sizeChange = (show: boolean) => {
+  appStore.setSize(show)
+}
+
+// 多语言图标
+const locale = ref(appStore.getLocale)
+
+const localeChange = (show: boolean) => {
+  appStore.setLocale(show)
+}
+
+// 标签页
+const tagsView = ref(appStore.getTagsView)
+
+const tagsViewChange = (show: boolean) => {
+  appStore.setTagsView(show)
+}
+
+// logo
+const logo = ref(appStore.getLogo)
+
+const logoChange = (show: boolean) => {
+  appStore.setLogo(show)
+}
+
+// 灰色模式
+const greyMode = ref(appStore.getGreyMode)
+
+const greyModeChange = (show: boolean) => {
+  appStore.setGreyMode(show)
+}
+</script>
+
+<template>
+  <div class="v-interface-display">
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.breadcrumb') }}</span>
+      <ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
+      <ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.collapseMenu') }}</span>
+      <ElSwitch v-model="collapse" @change="collapseChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
+      <ElSwitch v-model="hamburger" @change="hamburgerChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
+      <ElSwitch v-model="screenfull" @change="screenfullChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.sizeIcon') }}</span>
+      <ElSwitch v-model="size" @change="sizeChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.localeIcon') }}</span>
+      <ElSwitch v-model="locale" @change="localeChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.tagsView') }}</span>
+      <ElSwitch v-model="tagsView" @change="tagsViewChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.logo') }}</span>
+      <ElSwitch v-model="logo" @change="logoChange" />
+    </div>
+
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.greyMode') }}</span>
+      <ElSwitch v-model="greyMode" @change="greyModeChange" />
+    </div>
+  </div>
+</template>

+ 3 - 3
src/components/SizeDropdown/src/SizeDropdown.vue

@@ -9,13 +9,13 @@ const appStore = useAppStore()
 
 const sizeMap = computed(() => appStore.sizeMap)
 
-const setSize = (size: ElememtPlusSzie) => {
-  appStore.setSize(size)
+const setCurrentSize = (size: ElememtPlusSzie) => {
+  appStore.setCurrentSize(size)
 }
 </script>
 
 <template>
-  <ElDropdown trigger="click" @command="setSize">
+  <ElDropdown trigger="click" @command="setCurrentSize">
     <Icon
       :size="18"
       icon="mdi:format-size"

+ 2 - 2
src/components/TagsView/src/TagsView.vue

@@ -127,7 +127,7 @@ watch(
 <template>
   <div class="v-tags-view h-[var(--tags-view-height)] flex w-full">
     <span class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer">
-      <Icon icon="ant-design:left-outlined" color="#333" />
+      <Icon icon="ep:d-arrow-left" color="#333" />
     </span>
     <div class="overflow-hidden flex-1">
       <ElScrollbar>
@@ -216,7 +216,7 @@ watch(
       </ElScrollbar>
     </div>
     <span class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer">
-      <Icon icon="ant-design:right-outlined" color="#333" />
+      <Icon icon="ep:d-arrow-right" color="#333" />
     </span>
     <span
       class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer"

+ 47 - 26
src/config/app.ts

@@ -5,47 +5,68 @@ const { wsCache } = useCache()
 export type LayoutType = 'classic' | 'leftTop' | 'top' | 'test'
 
 export interface AppState {
+  breadcrumb: boolean
+  breadcrumbIcon: boolean
   collapse: boolean
-  showTags: boolean
-  showLogo: boolean
-  showNavbar: boolean
-  fixedHeader: boolean
+  hamburger: boolean
+  screenfull: boolean
+  size: boolean
+  locale: boolean
+  tagsView: boolean
+  logo: boolean
+  greyMode: boolean
+
   layout: LayoutType
-  showBreadcrumb: boolean
-  showHamburger: boolean
-  showScreenfull: boolean
-  showUserInfo: boolean
   title: string
   logoTitle: string
   userInfo: string
-  greyMode: boolean
-  showBackTop: boolean
-  showMenuTab: boolean
   isDark: boolean
-  size: ElememtPlusSzie
+  currentSize: ElememtPlusSzie
   sizeMap: ElememtPlusSzie[]
   mobile: boolean
+  theme: Recordable
 }
 
 export const appModules: AppState = {
-  collapse: false, // 菜单栏是否栏缩收
-  showLogo: true, // 是否显示logo
-  showTags: true, // 是否显示标签栏
-  showNavbar: true, // 是否显示navbar
-  fixedHeader: true, // 是否固定header
+  breadcrumb: true, // 面包屑
+  breadcrumbIcon: true, // 面包屑图标
+  collapse: false, // 折叠菜单
+  hamburger: true, // 折叠图标
+  screenfull: true, // 全屏图标
+  size: true, // 尺寸图标
+  locale: true, // 多语言图标
+  tagsView: true, // 标签页
+  logo: true, // logo
+  greyMode: false, // 是否开始灰色模式,用于特殊悼念日
+
   layout: 'classic', // layout布局
-  showBreadcrumb: true, // 是否显示面包屑
-  showHamburger: true, // 是否显示侧边栏缩收按钮
-  showScreenfull: true, // 是否全屏按钮
-  showUserInfo: true, // 是否显示用户头像
   title: 'butterfly-admin', // 标题
   logoTitle: 'ButterflyAdmin', // logo标题
   userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
-  greyMode: false, // 是否开始灰色模式,用于特殊悼念日
-  showBackTop: true, // 是否显示回到顶部
-  showMenuTab: false, // 是否固定一级菜单
   isDark: wsCache.get('isDark') || false, // 是否是暗黑模式
-  size: wsCache.get('default') || 'default', // 组件尺寸
+  currentSize: wsCache.get('default') || 'default', // 组件尺寸
   sizeMap: ['default', 'large', 'small'],
-  mobile: false // 是否是移动端
+  mobile: false, // 是否是移动端
+  theme: wsCache.get('theme') || {
+    // 主题色
+    elColorPrimary: '#409eff',
+    // 左侧菜单边框颜色
+    leftMenuBorderColor: 'inherit',
+    // 左侧菜单背景颜色
+    leftMenuBgColor: '#001529',
+    // 左侧菜单浅色背景颜色
+    leftMenuBgLightColor: '#0f2438',
+    // 左侧菜单选中背景颜色
+    leftMenuBgActiveColor: 'var(--el-color-primary)',
+    // 左侧菜单收起选中背景颜色
+    leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
+    // 左侧菜单字体颜色
+    leftMenuTextColor: '#bfcbd9',
+    // 左侧菜单选中字体颜色
+    leftMenuTextActiveColor: '#fff',
+    // logo字体颜色
+    logoTitleTextColor: '#fff',
+    // logo边框颜色
+    logoBorderColor: 'inherit'
+  }
 }

+ 2 - 3
src/config/locale.ts

@@ -8,14 +8,13 @@ export const elLocaleMap = {
   'zh-CN': zhCn,
   en: en
 }
-
 export interface LocaleState {
-  locale: LocaleDropdownType
+  currentLocale: LocaleDropdownType
   localeMap: LocaleDropdownType[]
 }
 
 export const localeModules: LocaleState = {
-  locale: {
+  currentLocale: {
     lang: wsCache.get('lang') || 'zh-CN',
     elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']
   },

+ 1 - 1
src/hooks/web/useLocale.ts

@@ -10,7 +10,7 @@ const setI18nLanguage = (locale: LocaleType) => {
   } else {
     ;(i18n.global.locale as any).value = locale
   }
-  localeStore.setLocale({
+  localeStore.setCurrentLocale({
     lang: locale
   })
   setHtmlPageLang(locale)

+ 31 - 8
src/layout/Layout.vue

@@ -15,10 +15,30 @@ import AppView from './components/AppView.vue'
 
 const appStore = useAppStore()
 
+// 是否是移动端
 const mobile = computed(() => appStore.getMobile)
 
+// 面包屑
+const breadcrumb = computed(() => appStore.getBreadcrumb)
+
+// 菜单折叠
 const collapse = computed(() => appStore.getCollapse)
 
+// 折叠图标
+const hamburger = computed(() => appStore.getHamburger)
+
+// 全屏图标
+const screenfull = computed(() => appStore.getScreenfull)
+
+// 尺寸图标
+const size = computed(() => appStore.getSize)
+
+// 多语言图标
+const locale = computed(() => appStore.getLocale)
+
+// 标签页
+const tagsView = computed(() => appStore.getTagsView)
+
 const classSuffix = computed(() => appStore.getLayout)
 
 const handleClickOutside = () => {
@@ -57,19 +77,22 @@ export default defineComponent({
             ]}
           >
             <div class="h-full flex items-center">
-              <Collapse class="header__tigger"></Collapse>
-              <Breadcrumb class="<md:hidden"></Breadcrumb>
+              {hamburger.value ? <Collapse class="header__tigger"></Collapse> : undefined}
+              {breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
             </div>
             <div class="h-full flex items-center">
-              <Screenfull class="header__tigger"></Screenfull>
-              <SizeDropdown class="header__tigger"></SizeDropdown>
-              <LocaleDropdown class="header__tigger"></LocaleDropdown>
+              {screenfull.value ? <Screenfull class="header__tigger"></Screenfull> : undefined}
+              {size.value ? <SizeDropdown class="header__tigger"></SizeDropdown> : undefined}
+              {locale.value ? <LocaleDropdown class="header__tigger"></LocaleDropdown> : undefined}
               <UserInfo class="header__tigger"></UserInfo>
             </div>
           </div>
-          <div class="v-app-right__tags-view relative">
-            <TagsView></TagsView>
-          </div>
+          {tagsView.value ? (
+            <div class="v-app-right__tags-view relative">
+              <TagsView></TagsView>
+            </div>
+          ) : undefined}
+
           <AppView></AppView>
         </div>
 

+ 18 - 3
src/locales/en.ts

@@ -19,6 +19,24 @@ export default {
     closeOther: 'Close other',
     closeAll: 'Close all'
   },
+  setting: {
+    projectSetting: 'Project setting',
+    theme: 'Theme',
+    layout: 'Layout',
+    systemTheme: 'System theme',
+    menuTheme: 'Menu theme',
+    interfaceDisplay: 'Interface display',
+    breadcrumb: 'Breadcrumb',
+    breadcrumbIcon: 'Breadcrumb icon',
+    collapseMenu: 'Collapse menu',
+    hamburgerIcon: 'Hamburger icon',
+    screenfullIcon: 'Screenfull icon',
+    sizeIcon: 'Size icon',
+    localeIcon: 'Locale icon',
+    tagsView: 'Tags view',
+    logo: 'Logo',
+    greyMode: 'Grey mode'
+  },
   size: {
     default: 'Default',
     large: 'Large',
@@ -46,9 +64,6 @@ export default {
     menu12: 'Menu1-2',
     menu2: 'Menu2'
   },
-  mock: {
-    loginErr: 'Wrong account or password'
-  },
   formDemo: {
     input: 'Input',
     inputNumber: 'InputNumber',

+ 18 - 3
src/locales/zh-CN.ts

@@ -19,6 +19,24 @@ export default {
     closeOther: '关闭其他标签页',
     closeAll: '关闭全部标签页'
   },
+  setting: {
+    projectSetting: '项目配置',
+    theme: '主题',
+    layout: '布局',
+    systemTheme: '系统主题',
+    menuTheme: '菜单主题',
+    interfaceDisplay: '界面显示',
+    breadcrumb: '面包屑',
+    breadcrumbIcon: '面包屑图标',
+    collapseMenu: '折叠菜单',
+    hamburgerIcon: '折叠图标',
+    screenfullIcon: '全屏图标',
+    sizeIcon: '尺寸图标',
+    localeIcon: '多语言图标',
+    tagsView: '标签页',
+    logo: '标志',
+    greyMode: '灰色模式'
+  },
   size: {
     default: '默认',
     large: '大',
@@ -46,9 +64,6 @@ export default {
     menu12: '菜单1-2',
     menu2: '菜单2'
   },
-  mock: {
-    loginErr: '账号或密码错误'
-  },
   formDemo: {
     input: '输入框',
     inputNumber: '数字输入框',

+ 2 - 2
src/plugins/vueI18n/index.ts

@@ -8,14 +8,14 @@ export let i18n: ReturnType<typeof createI18n>
 
 const createI18nOptions = async (): Promise<I18nOptions> => {
   const localeStore = useLocaleStoreWithOut()
-  const locale = localeStore.getLocale
+  const locale = localeStore.getCurrentLocale
   const localeMap = localeStore.getLocaleMap
   const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
   const message = defaultLocal.default ?? {}
 
   setHtmlPageLang(locale.lang)
 
-  localeStore.setLocale({
+  localeStore.setCurrentLocale({
     lang: locale.lang
     // elLocale: elLocal
   })

+ 64 - 61
src/store/modules/app.ts

@@ -3,6 +3,7 @@ import { store } from '../index'
 import { useCache } from '@/hooks/web/useCache'
 import { appModules } from '@/config/app'
 import type { AppState, LayoutType } from '@/config/app'
+import { setCssVar, humpToUnderline } from '@/utils'
 
 const { wsCache } = useCache()
 
@@ -10,35 +11,39 @@ export const useAppStore = defineStore({
   id: 'app',
   state: (): AppState => appModules,
   getters: {
+    getBreadcrumb(): boolean {
+      return this.breadcrumb
+    },
+    getBreadcrumbIcon(): boolean {
+      return this.breadcrumbIcon
+    },
     getCollapse(): boolean {
       return this.collapse
     },
-    getShowLogo(): boolean {
-      return this.showLogo
-    },
-    getShowTags(): boolean {
-      return this.showTags
+    getHamburger(): boolean {
+      return this.hamburger
     },
-    getShowNavbar(): boolean {
-      return this.showNavbar
+    getScreenfull(): boolean {
+      return this.screenfull
     },
-    getFixedHeader(): boolean {
-      return this.fixedHeader
+    getSize(): boolean {
+      return this.size
     },
-    getLayout(): LayoutType {
-      return this.layout
+    getLocale(): boolean {
+      return this.locale
     },
-    getShowBreadcrumb(): boolean {
-      return this.showBreadcrumb
+    getTagsView(): boolean {
+      return this.tagsView
     },
-    getShowHamburger(): boolean {
-      return this.showHamburger
+    getLogo(): boolean {
+      return this.logo
     },
-    getShowScreenfull(): boolean {
-      return this.showScreenfull
+    getGreyMode(): boolean {
+      return this.greyMode
     },
-    getShowUserInfo(): boolean {
-      return this.showUserInfo
+
+    getLayout(): LayoutType {
+      return this.layout
     },
     getTitle(): string {
       return this.title
@@ -49,58 +54,56 @@ export const useAppStore = defineStore({
     getUserInfo(): string {
       return this.userInfo
     },
-    getGreyMode(): boolean {
-      return this.greyMode
-    },
-    getShowBackTop(): boolean {
-      return this.showBackTop
-    },
-    getShowMenuTab(): boolean {
-      return this.showMenuTab
-    },
     getIsDark(): boolean {
       return this.isDark
     },
-    getSize(): ElememtPlusSzie {
-      return this.size
+    getCurrentSize(): ElememtPlusSzie {
+      return this.currentSize
     },
     getSizeMap(): ElememtPlusSzie[] {
       return this.sizeMap
     },
     getMobile(): boolean {
       return this.mobile
+    },
+    getTheme(): Recordable {
+      return this.theme
     }
   },
   actions: {
+    setBreadcrumb(breadcrumb: boolean) {
+      this.breadcrumb = breadcrumb
+    },
+    setBreadcrumbIcon(breadcrumbIcon: boolean) {
+      this.breadcrumbIcon = breadcrumbIcon
+    },
     setCollapse(collapse: boolean) {
       this.collapse = collapse
     },
-    setShowLogo(showLogo: boolean) {
-      this.showLogo = showLogo
-    },
-    setShowTags(showTags: boolean) {
-      this.showTags = showTags
+    setHamburger(hamburger: boolean) {
+      this.hamburger = hamburger
     },
-    setShowNavbar(showNavbar: boolean) {
-      this.showNavbar = showNavbar
+    setScreenfull(screenfull: boolean) {
+      this.screenfull = screenfull
     },
-    setFixedHeader(fixedHeader: boolean) {
-      this.fixedHeader = fixedHeader
+    setSize(size: boolean) {
+      this.size = size
     },
-    setLayout(layout: LayoutType) {
-      this.layout = layout
+    setLocale(locale: boolean) {
+      this.locale = locale
     },
-    setBreadcrumb(showBreadcrumb: boolean) {
-      this.showBreadcrumb = showBreadcrumb
+    setTagsView(tagsView: boolean) {
+      this.tagsView = tagsView
     },
-    setHamburger(showHamburger: boolean) {
-      this.showHamburger = showHamburger
+    setLogo(logo: boolean) {
+      this.logo = logo
     },
-    setScreenfull(showScreenfull: boolean) {
-      this.showScreenfull = showScreenfull
+    setGreyMode(greyMode: boolean) {
+      this.greyMode = greyMode
     },
-    setUserInfo(showUserInfo: boolean) {
-      this.showUserInfo = showUserInfo
+
+    setLayout(layout: LayoutType) {
+      this.layout = layout
     },
     setTitle(title: string) {
       this.title = title
@@ -108,15 +111,6 @@ export const useAppStore = defineStore({
     setLogoTitle(logoTitle: string) {
       this.logoTitle = logoTitle
     },
-    setGreyMode(greyMode: boolean) {
-      this.greyMode = greyMode
-    },
-    setShowBackTop(showBackTop: boolean) {
-      this.showBackTop = showBackTop
-    },
-    setShowMenuTab(showMenuTab: boolean) {
-      this.showMenuTab = showMenuTab
-    },
     setIsDark(isDark: boolean) {
       this.isDark = isDark
       if (this.isDark) {
@@ -128,12 +122,21 @@ export const useAppStore = defineStore({
       }
       wsCache.set('isDark', this.isDark)
     },
-    setSize(size: ElememtPlusSzie) {
-      this.size = size
-      wsCache.set('size', this.size)
+    setCurrentSize(currentSize: ElememtPlusSzie) {
+      this.currentSize = currentSize
+      wsCache.set('currentSize', this.currentSize)
     },
     setMobile(mobile: boolean) {
       this.mobile = mobile
+    },
+    setTheme(theme: Recordable) {
+      this.theme = Object.assign(this.theme, theme)
+      wsCache.set('theme', this.theme)
+    },
+    setCssVarTheme() {
+      for (const key in this.theme) {
+        setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
+      }
     }
   }
 })

+ 5 - 5
src/store/modules/locale.ts

@@ -10,18 +10,18 @@ export const useLocaleStore = defineStore({
   id: 'locales',
   state: (): LocaleState => localeModules,
   getters: {
-    getLocale(): LocaleDropdownType {
-      return this.locale
+    getCurrentLocale(): LocaleDropdownType {
+      return this.currentLocale
     },
     getLocaleMap(): LocaleDropdownType[] {
       return this.localeMap
     }
   },
   actions: {
-    setLocale(localeMap: LocaleDropdownType) {
+    setCurrentLocale(localeMap: LocaleDropdownType) {
       // this.locale = Object.assign(this.locale, localeMap)
-      this.locale.lang = localeMap?.lang
-      this.locale.elLocale = elLocaleMap[localeMap?.lang]
+      this.currentLocale.lang = localeMap?.lang
+      this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
       wsCache.set('lang', localeMap?.lang)
     }
   }

+ 5 - 1
src/styles/var.css

@@ -2,6 +2,8 @@
   --dark-bg-color: #293146;
 
   /* left menu start */
+  --left-menu-border-color: 'inherit';
+
   --left-menu-max-width: 200px;
 
   --left-menu-min-width: 64px;
@@ -23,10 +25,12 @@
   --logo-height: 50px;
 
   --logo-title-text-color: #fff;
+
+  --logo-border-color: 'inherit';
   /* logo end */
 
   /* header start */
-  --top-tool-height: 40px;
+  --top-tool-height: var(--logo-height);
 
   --top-tool-p-x: 0;
 

+ 4 - 2
src/utils/color.ts

@@ -30,7 +30,7 @@ export const rgbToHex = (r: number, g: number, b: number) => {
  * @param {string} hex The color to transform
  * @returns The RGB representation of the passed color
  */
-export const hexToRGB = (hex: string) => {
+export const hexToRGB = (hex: string, opacity?: number) => {
   let sHex = hex.toLowerCase()
   if (isHexColor(hex)) {
     if (sHex.length === 4) {
@@ -44,7 +44,9 @@ export const hexToRGB = (hex: string) => {
     for (let i = 1; i < 7; i += 2) {
       sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2)))
     }
-    return 'RGB(' + sColorChange.join(',') + ')'
+    return opacity
+      ? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')'
+      : 'RGB(' + sColorChange.join(',') + ')'
   }
   return sHex
 }

+ 4 - 0
src/utils/index.ts

@@ -59,3 +59,7 @@ export const findIndex = <T = Recordable>(ary: Array<T>, fn: Fn): number => {
   })
   return index
 }
+
+export const trim = (str: string) => {
+  return str.replace(/(^\s*)|(\s*$)/g, '')
+}

+ 1 - 1
src/views/Login/Login.vue

@@ -44,7 +44,7 @@ const { t } = useI18n()
 
           <div class="flex justify-end items-center space-x-10px">
             <ThemeSwitch />
-            <LocaleDropdown class="<xl:!text-white dark:!text-white" />
+            <LocaleDropdown class="<xl:text-white dark:text-white" />
           </div>
         </div>
         <Transition appear enter-active-class="animate__animated animate__bounceInRight">