Browse Source

feat: new menu and top bar color selection color matching

vben 4 years ago
parent
commit
7692ffb95b

+ 1 - 0
CHANGELOG.zh_CN.md

@@ -4,6 +4,7 @@
 
 
 - 表单项的`componentsProps`支持函数类型
 - 表单项的`componentsProps`支持函数类型
 - 菜单新增 tag 显示
 - 菜单新增 tag 显示
+- 新增菜单及顶栏颜色选择配色
 
 
 ### ⚡ Performance Improvements
 ### ⚡ Performance Improvements
 
 

+ 1 - 1
index.html

@@ -43,7 +43,7 @@
 
 
       .app-loading .g-loading {
       .app-loading .g-loading {
         display: block;
         display: block;
-        width: 64px;
+        width: 48px;
         margin: 30px auto;
         margin: 30px auto;
         -webkit-animation: load 1.2s linear infinite;
         -webkit-animation: load 1.2s linear infinite;
         animation: load 1.2s linear infinite;
         animation: load 1.2s linear infinite;

+ 15 - 17
src/components/Menu/src/BasicMenu.tsx

@@ -1,7 +1,7 @@
 import type { MenuState } from './types';
 import type { MenuState } from './types';
 import type { Menu as MenuType } from '/@/router/types';
 import type { Menu as MenuType } from '/@/router/types';
 
 
-import { computed, defineComponent, unref, reactive, toRef, watch, onMounted, ref } from 'vue';
+import { computed, defineComponent, unref, reactive, watch, onMounted, ref, toRefs } from 'vue';
 import { Menu } from 'ant-design-vue';
 import { Menu } from 'ant-design-vue';
 import SearchInput from './SearchInput.vue';
 import SearchInput from './SearchInput.vue';
 import MenuContent from './MenuContent';
 import MenuContent from './MenuContent';
@@ -40,8 +40,10 @@ export default defineComponent({
     });
     });
     const { currentRoute } = useRouter();
     const { currentRoute } = useRouter();
 
 
+    const { items, flatItems, isAppMenu, mode, accordion } = toRefs(props);
+
     const { handleInputChange, handleInputClick } = useSearchInput({
     const { handleInputChange, handleInputClick } = useSearchInput({
-      flatMenusRef: toRef(props, 'flatItems'),
+      flatMenusRef: flatItems,
       emit: emit,
       emit: emit,
       menuState,
       menuState,
       handleMenuChange,
       handleMenuChange,
@@ -49,11 +51,11 @@ export default defineComponent({
 
 
     const { handleOpenChange, resetKeys, setOpenKeys } = useOpenKeys(
     const { handleOpenChange, resetKeys, setOpenKeys } = useOpenKeys(
       menuState,
       menuState,
-      toRef(props, 'items'),
+      items,
-      toRef(props, 'flatItems'),
+      flatItems,
-      toRef(props, 'isAppMenu'),
+      isAppMenu,
-      toRef(props, 'mode'),
+      mode,
-      toRef(props, 'accordion')
+      accordion
     );
     );
 
 
     const getOpenKeys = computed(() => {
     const getOpenKeys = computed(() => {
@@ -98,6 +100,8 @@ export default defineComponent({
       return cls;
       return cls;
     });
     });
 
 
+    const showTitle = computed(() => props.collapsedShowTitle && menuStore.getCollapsedState);
+
     watch(
     watch(
       () => currentRoute.value.name,
       () => currentRoute.value.name,
       (name: string) => {
       (name: string) => {
@@ -130,9 +134,7 @@ export default defineComponent({
       const { beforeClickFn } = props;
       const { beforeClickFn } = props;
       if (beforeClickFn && isFunction(beforeClickFn)) {
       if (beforeClickFn && isFunction(beforeClickFn)) {
         const flag = await beforeClickFn(menu);
         const flag = await beforeClickFn(menu);
-        if (!flag) {
+        if (!flag) return;
-          return;
-        }
       }
       }
       const { path } = menu;
       const { path } = menu;
       menuState.selectedKeys = [path];
       menuState.selectedKeys = [path];
@@ -141,9 +143,7 @@ export default defineComponent({
 
 
     function handleMenuChange() {
     function handleMenuChange() {
       const { flatItems } = props;
       const { flatItems } = props;
-      if (!unref(flatItems) || flatItems.length === 0) {
+      if (!unref(flatItems) || flatItems.length === 0) return;
-        return;
-      }
       const findMenu = flatItems.find((menu) => menu.path === unref(currentRoute).path);
       const findMenu = flatItems.find((menu) => menu.path === unref(currentRoute).path);
       if (findMenu) {
       if (findMenu) {
         if (menuState.mode !== MenuModeEnum.HORIZONTAL) {
         if (menuState.mode !== MenuModeEnum.HORIZONTAL) {
@@ -155,10 +155,6 @@ export default defineComponent({
       }
       }
     }
     }
 
 
-    const showTitle = computed(() => {
-      return props.collapsedShowTitle && menuStore.getCollapsedState;
-    });
-
     // render menu item
     // render menu item
     function renderMenuItem(menuList?: MenuType[], index = 1) {
     function renderMenuItem(menuList?: MenuType[], index = 1) {
       if (!menuList) return;
       if (!menuList) return;
@@ -183,6 +179,7 @@ export default defineComponent({
                 <MenuContent
                 <MenuContent
                   item={menu}
                   item={menu}
                   level={index}
                   level={index}
+                  isTop={props.isTop}
                   showTitle={unref(showTitle)}
                   showTitle={unref(showTitle)}
                   searchValue={menuState.searchValue}
                   searchValue={menuState.searchValue}
                 />,
                 />,
@@ -198,6 +195,7 @@ export default defineComponent({
                   showTitle={unref(showTitle)}
                   showTitle={unref(showTitle)}
                   item={menu}
                   item={menu}
                   level={index}
                   level={index}
+                  isTop={props.isTop}
                   searchValue={menuState.searchValue}
                   searchValue={menuState.searchValue}
                 />,
                 />,
               ],
               ],

+ 8 - 2
src/components/Menu/src/MenuContent.tsx

@@ -26,6 +26,10 @@ export default defineComponent({
       type: Number as PropType<number>,
       type: Number as PropType<number>,
       default: 0,
       default: 0,
     },
     },
+    isTop: {
+      type: Boolean as PropType<boolean>,
+      default: true,
+    },
   },
   },
   setup(props) {
   setup(props) {
     /**
     /**
@@ -56,14 +60,16 @@ export default defineComponent({
       if (!props.item) {
       if (!props.item) {
         return null;
         return null;
       }
       }
-      const { showTitle } = props;
+      const { showTitle, isTop } = props;
       const { name, icon } = props.item;
       const { name, icon } = props.item;
       const searchValue = props.searchValue || '';
       const searchValue = props.searchValue || '';
       const index = name.indexOf(searchValue);
       const index = name.indexOf(searchValue);
 
 
       const beforeStr = name.substr(0, index);
       const beforeStr = name.substr(0, index);
       const afterStr = name.substr(index + searchValue.length);
       const afterStr = name.substr(index + searchValue.length);
-      const cls = showTitle ? 'show-title' : 'basic-menu__name';
+      let cls = showTitle ? ['show-title'] : ['basic-menu__name'];
+
+      isTop && !showTitle && (cls = []);
       return (
       return (
         <>
         <>
           {renderIcon(icon!)}
           {renderIcon(icon!)}

+ 1 - 1
src/components/Menu/src/SearchInput.vue

@@ -102,7 +102,7 @@
 
 
   .set-bg() {
   .set-bg() {
     color: #fff;
     color: #fff;
-    background: @input-dark-bg-color;
+    background: @sider-dark-lighten-1-bg-color;
     border: 0;
     border: 0;
     outline: none;
     outline: none;
   }
   }

+ 12 - 12
src/components/Menu/src/index.less

@@ -52,10 +52,11 @@
   // collapsed show title end
   // collapsed show title end
   .ant-menu-submenu-title {
   .ant-menu-submenu-title {
     > .basic-menu__name {
     > .basic-menu__name {
-      display: flex;
+      .basic-menu__tag {
-      width: 100%;
+        float: right;
-      justify-content: space-between;
+        margin-top: @app-menu-item-height / 2;
-      align-items: center;
+        transform: translate(0%, -50%);
+      }
     }
     }
   }
   }
 
 
@@ -254,7 +255,7 @@
   // 层级样式
   // 层级样式
   &.ant-menu-dark:not(.basic-menu__sidebar-hor) {
   &.ant-menu-dark:not(.basic-menu__sidebar-hor) {
     overflow-x: hidden;
     overflow-x: hidden;
-    background: @menu-item-dark-bg-color;
+    background: @sider-dark-bg-color;
     .active-menu-style();
     .active-menu-style();
 
 
     .ant-menu-item.ant-menu-item-selected.basic-menu-menu-item__level1,
     .ant-menu-item.ant-menu-item-selected.basic-menu-menu-item__level1,
@@ -263,21 +264,20 @@
     }
     }
 
 
     .basic-menu-item__level1 {
     .basic-menu-item__level1 {
-      background-color: @menu-item-dark-bg-color;
+      background-color: @sider-dark-bg-color;
 
 
       > .ant-menu-sub > li {
       > .ant-menu-sub > li {
-        background-color: lighten(@menu-item-dark-bg-color, 6%);
+        background-color: @sider-dark-lighten-1-bg-color;
       }
       }
     }
     }
 
 
     .basic-menu-item__level2:not(.ant-menu-item-selected),
     .basic-menu-item__level2:not(.ant-menu-item-selected),
     .ant-menu-sub {
     .ant-menu-sub {
-      background-color: lighten(@menu-item-dark-bg-color, 6%);
+      background-color: @sider-dark-lighten-1-bg-color;
-      // background-color: @sub-menu-item-dark-bg-color;
     }
     }
 
 
     .basic-menu-item__level3:not(.ant-menu-item-selected) {
     .basic-menu-item__level3:not(.ant-menu-item-selected) {
-      background-color: lighten(@menu-item-dark-bg-color, 10%);
+      background-color: @sider-dark-lighten-2-bg-color;
     }
     }
 
 
     .ant-menu-submenu-title {
     .ant-menu-submenu-title {
@@ -290,7 +290,7 @@
     &.ant-menu-inline-collapsed {
     &.ant-menu-inline-collapsed {
       .ant-menu-submenu-selected,
       .ant-menu-submenu-selected,
       .ant-menu-item-selected {
       .ant-menu-item-selected {
-        background: darken(@menu-item-dark-bg-color, 6%) !important;
+        background: @sider-dark-darken-bg-color !important;
       }
       }
     }
     }
   }
   }
@@ -359,7 +359,7 @@
 .ant-menu-dark {
 .ant-menu-dark {
   &.ant-menu-submenu-popup {
   &.ant-menu-submenu-popup {
     > ul {
     > ul {
-      background: @menu-item-dark-bg-color;
+      background: @sider-dark-bg-color;
     }
     }
 
 
     .active-menu-style();
     .active-menu-style();

+ 23 - 6
src/design/color.less

@@ -1,3 +1,17 @@
+:root {
+  // header
+  --header-bg-color: #394664;
+  --header-bg-hover-color: #273352;
+  --header-active-menu-bg-color: #273352;
+
+  // sider
+  --sider-dark-bg-color: #273352;
+  --sider-dark-darken-bg-color: #273352;
+  --sider-dark-lighten-1-bg-color: #273352;
+  --sider-dark-lighten-2-bg-color: #273352;
+  --sider-dark-lighten-3-bg-color: #273352;
+}
+
 @white: #fff;
 @white: #fff;
 @info-color: @primary-color;
 @info-color: @primary-color;
 
 
@@ -53,21 +67,24 @@
 // ==============Header=============
 // ==============Header=============
 // =================================
 // =================================
 
 
-@header-dark-bg-color: #394664;
+@header-dark-bg-color: var(--header-bg-color);
-@header-dark-bg-hover-color: #273352;
+@header-dark-bg-hover-color: var(--header-bg-hover-color);
 @header-light-bg-hover-color: #f6f6f6;
 @header-light-bg-hover-color: #f6f6f6;
 @header-light-desc-color: #7c8087;
 @header-light-desc-color: #7c8087;
 @header-light-bottom-border-color: #eee;
 @header-light-bottom-border-color: #eee;
+// top-menu
+@top-menu-active-bg-color: var(--header-active-menu-bg-color);
 
 
 // =================================
 // =================================
 // ==============Menu============
 // ==============Menu============
 // =================================
 // =================================
 
 
 // let -menu
 // let -menu
-@menu-item-dark-bg-color: #273352;
+@sider-dark-bg-color: var(--sider-dark-bg-color);
-
+@sider-dark-darken-bg-color: var(--sider-dark-darken-bg-color);
-// top-menu
+@sider-dark-lighten-1-bg-color: var(--sider-dark-lighten-1-bg-color);
-@top-menu-active-bg-color: #273352;
+@sider-dark-lighten-2-bg-color: var(--sider-dark-lighten-2-bg-color);
+@sider-dark-lighten-3-bg-color: var(--sider-dark-lighten-3-bg-color);
 
 
 // trigger
 // trigger
 @trigger-dark-hover-bg-color: rgba(255, 255, 255, 0.2);
 @trigger-dark-hover-bg-color: rgba(255, 255, 255, 0.2);

+ 2 - 3
src/layouts/default/header/LayoutHeader.tsx

@@ -79,9 +79,8 @@ export default defineComponent({
     });
     });
 
 
     const showHeaderTrigger = computed(() => {
     const showHeaderTrigger = computed(() => {
-      const { show, trigger, hidden } = unref(getProjectConfigRef).menuSetting;
+      const { show, trigger, hidden, type } = unref(getProjectConfigRef).menuSetting;
-
+      if (type === MenuTypeEnum.TOP_MENU || !show || !hidden) return false;
-      if (!show || !hidden) return false;
       return trigger === TriggerEnum.HEADER;
       return trigger === TriggerEnum.HEADER;
     });
     });
 
 

+ 1 - 1
src/layouts/default/index.less

@@ -41,7 +41,7 @@
     background-size: 100% 100%;
     background-size: 100% 100%;
 
 
     &.ant-layout-sider-dark {
     &.ant-layout-sider-dark {
-      background: @menu-item-dark-bg-color;
+      background: @sider-dark-bg-color;
     }
     }
 
 
     &:not(.ant-layout-sider-dark) {
     &:not(.ant-layout-sider-dark) {

+ 49 - 32
src/layouts/default/index.tsx

@@ -25,13 +25,9 @@ export default defineComponent({
 
 
     const { getFullContent } = useFullContent();
     const { getFullContent } = useFullContent();
 
 
-    const getProjectConfigRef = computed(() => {
+    const getProjectConfigRef = computed(() => appStore.getProjectConfig);
-      return appStore.getProjectConfig;
-    });
 
 
-    const getLockMainScrollStateRef = computed(() => {
+    const getLockMainScrollStateRef = computed(() => appStore.getLockMainScrollState);
-      return appStore.getLockMainScrollState;
-    });
 
 
     const showHeaderRef = computed(() => {
     const showHeaderRef = computed(() => {
       const {
       const {
@@ -47,6 +43,12 @@ export default defineComponent({
       return type !== MenuTypeEnum.SIDEBAR && unref(showHeaderRef);
       return type !== MenuTypeEnum.SIDEBAR && unref(showHeaderRef);
     });
     });
 
 
+    const getIsLockRef = computed(() => {
+      const { getLockInfo } = appStore;
+      const { isLock } = getLockInfo;
+      return isLock;
+    });
+
     const showSideBarRef = computed(() => {
     const showSideBarRef = computed(() => {
       const {
       const {
         menuSetting: { show, mode, split },
         menuSetting: { show, mode, split },
@@ -54,59 +56,74 @@ export default defineComponent({
       return split || (show && mode !== MenuModeEnum.HORIZONTAL && !unref(getFullContent));
       return split || (show && mode !== MenuModeEnum.HORIZONTAL && !unref(getFullContent));
     });
     });
 
 
-    function getTarget(): any {
+    const showFullHeaderRef = computed(() => {
-      const {
+      return !unref(getFullContent) && unref(isShowMixHeaderRef) && unref(showHeaderRef);
-        headerSetting: { fixed },
+    });
-      } = unref(getProjectConfigRef);
-      return document.querySelector(`.default-layout__${fixed ? 'main' : 'content'}`);
-    }
 
 
-    return () => {
+    const showInsetHeaderRef = computed(() => {
-      const { getLockInfo } = appStore;
+      return !unref(getFullContent) && !unref(isShowMixHeaderRef) && unref(showHeaderRef);
+    });
+
+    const fixedHeaderClsRef = computed(() => {
       const {
       const {
-        useOpenBackTop,
-        showSettingButton,
-        multiTabsSetting: { show: showTabs },
         headerSetting: { fixed },
         headerSetting: { fixed },
-        menuSetting: { split, hidden },
       } = unref(getProjectConfigRef);
       } = unref(getProjectConfigRef);
-
       const fixedHeaderCls = fixed
       const fixedHeaderCls = fixed
         ? 'fixed' + (unref(getLockMainScrollStateRef) ? ' lock' : '')
         ? 'fixed' + (unref(getLockMainScrollStateRef) ? ' lock' : '')
         : '';
         : '';
+      return fixedHeaderCls;
+    });
 
 
-      const { isLock } = getLockInfo;
+    const showTabsRef = computed(() => {
+      const {
+        multiTabsSetting: { show },
+      } = unref(getProjectConfigRef);
+      return show && !unref(getFullContent);
+    });
+
+    const showClassSideBarRef = computed(() => {
+      const {
+        menuSetting: { split, hidden },
+      } = unref(getProjectConfigRef);
+      return split ? hidden : true;
+    });
 
 
-      const showSideBar = split ? hidden : true;
+    function getTarget(): any {
+      const {
+        headerSetting: { fixed },
+      } = unref(getProjectConfigRef);
+      return document.querySelector(`.default-layout__${fixed ? 'main' : 'content'}`);
+    }
+
+    return () => {
+      const { useOpenBackTop, showSettingButton } = unref(getProjectConfigRef);
       return (
       return (
         <Layout class="default-layout relative">
         <Layout class="default-layout relative">
           {() => (
           {() => (
             <>
             <>
               {/* lock page */}
               {/* lock page */}
-              {isLock && <LockPage />}
+              {unref(getIsLockRef) && <LockPage />}
               {/* back top */}
               {/* back top */}
               {useOpenBackTop && <BackTop target={getTarget} />}
               {useOpenBackTop && <BackTop target={getTarget} />}
               {/* open setting drawer */}
               {/* open setting drawer */}
               {showSettingButton && <SettingBtn />}
               {showSettingButton && <SettingBtn />}
 
 
-              {!unref(getFullContent) && unref(isShowMixHeaderRef) && unref(showHeaderRef) && (
+              {unref(showFullHeaderRef) && <LayoutHeader />}
-                <LayoutHeader />
-              )}
 
 
               <Layout>
               <Layout>
                 {() => (
                 {() => (
                   <>
                   <>
-                    {unref(showSideBarRef) && <LayoutSideBar class={showSideBar ? '' : 'hidden'} />}
+                    {unref(showSideBarRef) && (
-                    <Layout class={[`default-layout__content`, fixedHeaderCls]}>
+                      <LayoutSideBar class={unref(showClassSideBarRef) ? '' : 'hidden'} />
+                    )}
+                    <Layout class={[`default-layout__content`, unref(fixedHeaderClsRef)]}>
                       {() => (
                       {() => (
                         <>
                         <>
-                          {!unref(getFullContent) &&
+                          {unref(showInsetHeaderRef) && <LayoutHeader />}
-                            !unref(isShowMixHeaderRef) &&
-                            unref(showHeaderRef) && <LayoutHeader />}
 
 
-                          {showTabs && !unref(getFullContent) && <MultipleTabs />}
+                          {unref(showTabsRef) && <MultipleTabs />}
 
 
-                          <LayoutContent class={fixedHeaderCls} />
+                          <LayoutContent class={unref(fixedHeaderClsRef)} />
                         </>
                         </>
                       )}
                       )}
                     </Layout>
                     </Layout>

+ 70 - 20
src/layouts/default/setting/SettingDrawer.tsx

@@ -20,12 +20,12 @@ import { updateColorWeak, updateGrayMode } from '/@/setup/theme';
 import { baseHandler } from './handler';
 import { baseHandler } from './handler';
 import {
 import {
   HandlerEnum,
   HandlerEnum,
-  themeOptions,
   contentModeOptions,
   contentModeOptions,
   topMenuAlignOptions,
   topMenuAlignOptions,
   menuTriggerOptions,
   menuTriggerOptions,
   routerTransitionOptions,
   routerTransitionOptions,
 } from './const';
 } from './const';
+import { HEADER_PRESET_BG_COLOR_LIST, SIDE_BAR_BG_COLOR_LIST } from '/@/settings/colorSetting';
 
 
 interface SwitchOptions {
 interface SwitchOptions {
   config?: DeepPartial<ProjectConfig>;
   config?: DeepPartial<ProjectConfig>;
@@ -41,6 +41,11 @@ interface SelectConfig {
   handler?: Fn;
   handler?: Fn;
 }
 }
 
 
+interface ThemeOptions {
+  def?: string;
+  handler?: Fn;
+}
+
 export default defineComponent({
 export default defineComponent({
   name: 'SettingDrawer',
   name: 'SettingDrawer',
   setup(_, { attrs }) {
   setup(_, { attrs }) {
@@ -98,8 +103,7 @@ export default defineComponent({
 
 
     function renderSidebar() {
     function renderSidebar() {
       const {
       const {
-        headerSetting: { theme: headerTheme },
+        menuSetting: { type, split },
-        menuSetting: { type, theme: menuTheme, split },
       } = unref(getProjectConfigRef);
       } = unref(getProjectConfigRef);
 
 
       const typeList = ref([
       const typeList = ref([
@@ -154,22 +158,22 @@ export default defineComponent({
           def: split,
           def: split,
           disabled: !unref(getShowMenuRef) || type !== MenuTypeEnum.MIX,
           disabled: !unref(getShowMenuRef) || type !== MenuTypeEnum.MIX,
         }),
         }),
-        renderSelectItem('顶栏主题', {
+        // renderSelectItem('顶栏主题', {
-          handler: (e) => {
+        //   handler: (e) => {
-            baseHandler(HandlerEnum.HEADER_THEME, e);
+        //     baseHandler(HandlerEnum.HEADER_THEME, e);
-          },
+        //   },
-          def: headerTheme,
+        //   def: headerTheme,
-          options: themeOptions,
+        //   options: themeOptions,
-          disabled: !unref(getShowHeaderRef),
+        //   disabled: !unref(getShowHeaderRef),
-        }),
+        // }),
-        renderSelectItem('菜单主题', {
+        // renderSelectItem('菜单主题', {
-          handler: (e) => {
+        //   handler: (e) => {
-            baseHandler(HandlerEnum.MENU_THEME, e);
+        //     baseHandler(HandlerEnum.MENU_THEME, e);
-          },
+        //   },
-          def: menuTheme,
+        //   def: menuTheme,
-          options: themeOptions,
+        //   options: themeOptions,
-          disabled: !unref(getShowMenuRef),
+        //   disabled: !unref(getShowMenuRef),
-        }),
+        // }),
       ];
       ];
     }
     }
     /**
     /**
@@ -413,7 +417,6 @@ export default defineComponent({
       return (
       return (
         <div class={`setting-drawer__cell-item`}>
         <div class={`setting-drawer__cell-item`}>
           <span>{text}</span>
           <span>{text}</span>
-          {/* @ts-ignore */}
           <Select
           <Select
             {...opt}
             {...opt}
             disabled={disabled}
             disabled={disabled}
@@ -447,6 +450,50 @@ export default defineComponent({
       );
       );
     }
     }
 
 
+    function renderTheme() {
+      const { headerBgColor, menuBgColor } = unref(getProjectConfigRef);
+      return (
+        <>
+          <Divider>{() => '顶栏主题'}</Divider>
+          {renderThemeItem(HEADER_PRESET_BG_COLOR_LIST, {
+            def: headerBgColor,
+            handler: (e) => {
+              baseHandler(HandlerEnum.HEADER_THEME, e);
+            },
+          })}
+          <Divider>{() => '菜单主题'}</Divider>
+          {renderThemeItem(SIDE_BAR_BG_COLOR_LIST, {
+            def: menuBgColor,
+            handler: (e) => {
+              baseHandler(HandlerEnum.MENU_THEME, e);
+            },
+          })}
+        </>
+      );
+    }
+
+    function renderThemeItem(colorList: string[], opt: ThemeOptions) {
+      const { def, handler } = opt;
+      return (
+        <div class={`setting-drawer__theme-item`}>
+          {colorList.map((item) => {
+            return (
+              <span
+                onClick={() => handler && handler(item)}
+                key={item}
+                class={[def === item ? 'active' : '']}
+                style={{
+                  background: item,
+                }}
+              >
+                <CheckOutlined class="icon" />
+              </span>
+            );
+          })}
+        </div>
+      );
+    }
+
     return () => (
     return () => (
       <BasicDrawer {...attrs} title="项目配置" width={300} wrapClassName="setting-drawer">
       <BasicDrawer {...attrs} title="项目配置" width={300} wrapClassName="setting-drawer">
         {{
         {{
@@ -454,6 +501,9 @@ export default defineComponent({
             <>
             <>
               <Divider>{() => '导航栏模式'}</Divider>
               <Divider>{() => '导航栏模式'}</Divider>
               {renderSidebar()}
               {renderSidebar()}
+
+              {renderTheme()}
+
               <Divider>{() => '界面功能'}</Divider>
               <Divider>{() => '界面功能'}</Divider>
               {renderFeatures()}
               {renderFeatures()}
               <Divider>{() => '界面显示'}</Divider>
               <Divider>{() => '界面显示'}</Divider>

+ 22 - 16
src/layouts/default/setting/handler.ts

@@ -1,6 +1,11 @@
 import { HandlerEnum } from './const';
 import { HandlerEnum } from './const';
-import { MenuThemeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
+// import { MenuThemeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
-import { updateColorWeak, updateGrayMode } from '/@/setup/theme';
+import {
+  updateColorWeak,
+  updateGrayMode,
+  updateHeaderBgColor,
+  updateSidebarBgColor,
+} from '/@/setup/theme';
 import { appStore } from '/@/store/modules/app';
 import { appStore } from '/@/store/modules/app';
 import { ProjectConfig } from '/@/types/config';
 import { ProjectConfig } from '/@/types/config';
 
 
@@ -14,12 +19,12 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
     case HandlerEnum.CHANGE_LAYOUT:
     case HandlerEnum.CHANGE_LAYOUT:
       const { mode, type, split } = value;
       const { mode, type, split } = value;
       const splitOpt = split === undefined ? { split } : {};
       const splitOpt = split === undefined ? { split } : {};
-      let headerSetting = {};
+      // let headerSetting = {};
-      if (type === MenuTypeEnum.TOP_MENU) {
+      // if (type === MenuTypeEnum.TOP_MENU) {
-        headerSetting = {
+      //   headerSetting = {
-          theme: MenuThemeEnum.DARK,
+      //     theme: MenuThemeEnum.DARK,
-        };
+      //   };
-      }
+      // }
       return {
       return {
         menuSetting: {
         menuSetting: {
           mode,
           mode,
@@ -28,7 +33,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
           show: true,
           show: true,
           ...splitOpt,
           ...splitOpt,
         },
         },
-        headerSetting,
+        // headerSetting,
       };
       };
 
 
     case HandlerEnum.MENU_HAS_DRAG:
     case HandlerEnum.MENU_HAS_DRAG:
@@ -81,10 +86,12 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
         },
         },
       };
       };
     case HandlerEnum.MENU_THEME:
     case HandlerEnum.MENU_THEME:
+      updateSidebarBgColor(value);
       return {
       return {
-        menuSetting: {
+        menuBgColor: value,
-          theme: value,
+        // menuSetting: {
-        },
+        //   theme: value,
+        // },
       };
       };
     case HandlerEnum.MENU_SPLIT:
     case HandlerEnum.MENU_SPLIT:
       return {
       return {
@@ -150,7 +157,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
           showQuick: value,
           showQuick: value,
         },
         },
       };
       };
-    case HandlerEnum.TABS_SHOW_QUICK:
+    case HandlerEnum.TABS_SHOW_ICON:
       return {
       return {
         multiTabsSetting: {
         multiTabsSetting: {
           showIcon: value,
           showIcon: value,
@@ -163,10 +170,9 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
         },
         },
       };
       };
     case HandlerEnum.HEADER_THEME:
     case HandlerEnum.HEADER_THEME:
+      updateHeaderBgColor(value);
       return {
       return {
-        headerSetting: {
+        headerBgColor: value,
-          theme: value,
-        },
       };
       };
     case HandlerEnum.HEADER_FIXED:
     case HandlerEnum.HEADER_FIXED:
       return {
       return {

+ 8 - 7
src/layouts/default/setting/index.less

@@ -20,25 +20,26 @@
     display: flex;
     display: flex;
     flex-wrap: wrap;
     flex-wrap: wrap;
     margin: 16px 0;
     margin: 16px 0;
+    justify-content: space-around;
 
 
-    span {
+    > span {
-      display: inline-block;
       width: 20px;
       width: 20px;
       height: 20px;
       height: 20px;
-      margin-top: 10px;
-      margin-right: 10px;
       cursor: pointer;
       cursor: pointer;
-      border-radius: 4px;
+      border: 1px solid #ddd;
+      border-radius: 2px;
 
 
       svg {
       svg {
         display: none;
         display: none;
       }
       }
 
 
       &.active {
       &.active {
+        border: 1px solid lighten(@primary-color, 10%);
+
         svg {
         svg {
           display: inline-block;
           display: inline-block;
-          margin-left: 4px;
+          margin: 0 0 3px 3px;
-          font-size: 0.8em;
+          font-size: 12px;
           fill: @white;
           fill: @white;
         }
         }
       }
       }

+ 2 - 3
src/layouts/page/index.tsx

@@ -41,13 +41,12 @@ export default defineComponent({
                 // No longer show animations that are already in the tab
                 // No longer show animations that are already in the tab
                 const name = route.meta.inTab ? 'fade' : null;
                 const name = route.meta.inTab ? 'fade' : null;
 
 
-                // TODO add key?
                 const Content = openCache ? (
                 const Content = openCache ? (
                   <KeepAlive max={max} include={cacheTabs}>
                   <KeepAlive max={max} include={cacheTabs}>
-                    <Component key={route.path} />
+                    <Component key={route.fullPath} />
                   </KeepAlive>
                   </KeepAlive>
                 ) : (
                 ) : (
-                  <Component key={route.path} />
+                  <Component key={route.fullPath} />
                 );
                 );
                 return openRouterTransition ? (
                 return openRouterTransition ? (
                   <Transition
                   <Transition

+ 2 - 1
src/router/index.ts

@@ -3,7 +3,7 @@ import type { App } from 'vue';
 
 
 import { createRouter, createWebHashHistory } from 'vue-router';
 import { createRouter, createWebHashHistory } from 'vue-router';
 
 
-import { scrollWaiter } from '../utils/scrollWaiter';
+import { scrollWaiter } from './scrollWaiter';
 
 
 import { createGuard } from './guard/';
 import { createGuard } from './guard/';
 
 
@@ -13,6 +13,7 @@ import { basicRoutes } from './routes/';
 const router = createRouter({
 const router = createRouter({
   history: createWebHashHistory(),
   history: createWebHashHistory(),
   routes: basicRoutes as RouteRecordRaw[],
   routes: basicRoutes as RouteRecordRaw[],
+  strict: true,
   scrollBehavior: async (to, from, savedPosition) => {
   scrollBehavior: async (to, from, savedPosition) => {
     await scrollWaiter.wait();
     await scrollWaiter.wait();
     if (savedPosition) {
     if (savedPosition) {

+ 1 - 0
src/utils/scrollWaiter.ts → src/router/scrollWaiter.ts

@@ -1,3 +1,4 @@
+// see https://github.com/vuejs/vue-router-next/blob/master/playground/scrollWaiter.ts
 class ScrollQueue {
 class ScrollQueue {
   private resolve: (() => void) | null = null;
   private resolve: (() => void) | null = null;
   private promise: Promise<any> | null = null;
   private promise: Promise<any> | null = null;

+ 24 - 0
src/settings/colorSetting.ts

@@ -0,0 +1,24 @@
+// header preset color
+export const HEADER_PRESET_BG_COLOR_LIST: string[] = [
+  '#ffffff',
+  '#009688',
+  '#18bc9c',
+  '#1E9FFF',
+  '#018ffb',
+  '#409eff',
+  '#4e73df',
+  '#e74c3c',
+  '#f39c12',
+  '#394664',
+  '#001529',
+];
+
+// sider preset color
+export const SIDE_BAR_BG_COLOR_LIST: string[] = [
+  '#273352',
+  '#ffffff',
+  '#001529',
+  '#304156',
+  '#28333E',
+  '#344058',
+];

+ 11 - 2
src/settings/projectSetting.ts

@@ -7,6 +7,16 @@ import { isProdMode } from '/@/utils/env';
 
 
 // ! You need to clear the browser cache after the change
 // ! You need to clear the browser cache after the change
 const setting: ProjectConfig = {
 const setting: ProjectConfig = {
+  // color
+  // TODO 主题色
+  themeColor: primaryColor,
+
+  // header bg color
+  headerBgColor: '#ffffff',
+
+  // sidebar menu bg color
+  menuBgColor: '#273352',
+
   // Whether to show the configuration button
   // Whether to show the configuration button
   showSettingButton: true,
   showSettingButton: true,
   // 权限模式
   // 权限模式
@@ -15,8 +25,7 @@ const setting: ProjectConfig = {
   grayMode: false,
   grayMode: false,
   // 色弱模式
   // 色弱模式
   colorWeak: false,
   colorWeak: false,
-  // 主题色
+
-  themeColor: primaryColor,
   // 是否取消菜单,顶部,多标签页显示, 用于可能内嵌在别的系统内
   // 是否取消菜单,顶部,多标签页显示, 用于可能内嵌在别的系统内
   fullContent: false,
   fullContent: false,
   // content mode
   // content mode

+ 58 - 0
src/setup/theme/index.ts

@@ -1,9 +1,24 @@
+import useCssVar from '/@/hooks/web/useCssVar';
+import { isHexColor, colorIsDark, lighten, darken } from '/@/utils/color';
+import { appStore } from '/@/store/modules/app';
+import { MenuThemeEnum } from '/@/enums/menuEnum';
+
+const HEADER_BG_COLOR_VAR = '--header-bg-color';
+const HEADER_BG_HOVER_COLOR_VAR = '--header-bg-hover-color';
+const HEADER_MENU_ACTIVE_BG_COLOR_VAR = '--header-active-menu-bg-color';
+
+const SIDER_DARK_BG_COLOR = '--sider-dark-bg-color';
+const SIDER_DARK_DARKEN_BG_COLOR = '--sider-dark-darken-bg-color';
+const SIDER_LIGHTEN_1_BG_COLOR = '--sider-dark-lighten-1-bg-color';
+const SIDER_LIGHTEN_2_BG_COLOR = '--sider-dark-lighten-2-bg-color';
+
 function toggleClass(flag: boolean, clsName: string) {
 function toggleClass(flag: boolean, clsName: string) {
   const body = document.body;
   const body = document.body;
   let { className } = body;
   let { className } = body;
   className = className.replace(clsName, '');
   className = className.replace(clsName, '');
   document.body.className = flag ? `${className} ${clsName} ` : className;
   document.body.className = flag ? `${className} ${clsName} ` : className;
 }
 }
+
 export const updateColorWeak = (colorWeak: boolean) => {
 export const updateColorWeak = (colorWeak: boolean) => {
   toggleClass(colorWeak, 'color-weak');
   toggleClass(colorWeak, 'color-weak');
 };
 };
@@ -11,3 +26,46 @@ export const updateColorWeak = (colorWeak: boolean) => {
 export const updateGrayMode = (gray: boolean) => {
 export const updateGrayMode = (gray: boolean) => {
   toggleClass(gray, 'gray-mode');
   toggleClass(gray, 'gray-mode');
 };
 };
+
+export function updateHeaderBgColor(color: string) {
+  if (!isHexColor(color)) return;
+  const bgColorRef = useCssVar(HEADER_BG_COLOR_VAR);
+  const bgHoverColorRef = useCssVar(HEADER_BG_HOVER_COLOR_VAR);
+  const topMenuActiveBgColorRef = useCssVar(HEADER_MENU_ACTIVE_BG_COLOR_VAR);
+  // bg color
+  bgColorRef.value = color;
+  // hover color
+  const hoverColor = lighten(color, 6);
+  bgHoverColorRef.value = hoverColor;
+  topMenuActiveBgColorRef.value = hoverColor;
+
+  const isDark = colorIsDark(color);
+
+  appStore.commitProjectConfigState({
+    headerSetting: {
+      theme: isDark ? MenuThemeEnum.DARK : MenuThemeEnum.LIGHT,
+    },
+  });
+}
+
+export function updateSidebarBgColor(color: string) {
+  if (!isHexColor(color)) return;
+
+  const siderBgColor = useCssVar(SIDER_DARK_BG_COLOR);
+  const darkenBgColor = useCssVar(SIDER_DARK_DARKEN_BG_COLOR);
+  const lighten1Color = useCssVar(SIDER_LIGHTEN_1_BG_COLOR);
+  const lighten2Color = useCssVar(SIDER_LIGHTEN_2_BG_COLOR);
+
+  siderBgColor.value = color;
+  darkenBgColor.value = darken(color, 6);
+  lighten1Color.value = lighten(color, 4);
+  lighten2Color.value = lighten(color, 8);
+  // only #ffffff is light
+  const isLight = ['#fff', '#ffffff'].includes(color.toLowerCase());
+
+  appStore.commitProjectConfigState({
+    menuSetting: {
+      theme: isLight ? MenuThemeEnum.LIGHT : MenuThemeEnum.DARK,
+    },
+  });
+}

+ 4 - 0
src/types/config.d.ts

@@ -55,6 +55,10 @@ export interface HeaderSetting {
   showNotice: boolean;
   showNotice: boolean;
 }
 }
 export interface ProjectConfig {
 export interface ProjectConfig {
+  // header背景色
+  headerBgColor: string;
+  // 左侧菜单背景色
+  menuBgColor: string;
   // 是否显示配置按钮
   // 是否显示配置按钮
   showSettingButton: boolean;
   showSettingButton: boolean;
   // 权限模式
   // 权限模式

+ 9 - 2
src/useApp.ts

@@ -9,7 +9,12 @@ import { PROJ_CFG_KEY } from '/@/enums/cacheEnum';
 import projectSetting from '/@/settings/projectSetting';
 import projectSetting from '/@/settings/projectSetting';
 import { getLocal } from '/@/utils/helper/persistent';
 import { getLocal } from '/@/utils/helper/persistent';
 import { isUnDef, isNull } from '/@/utils/is';
 import { isUnDef, isNull } from '/@/utils/is';
-import { updateGrayMode, updateColorWeak } from '/@/setup/theme';
+import {
+  updateGrayMode,
+  updateColorWeak,
+  updateHeaderBgColor,
+  updateSidebarBgColor,
+} from '/@/setup/theme';
 
 
 import { appStore } from '/@/store/modules/app';
 import { appStore } from '/@/store/modules/app';
 import { useNetWork } from '/@/hooks/web/useNetWork';
 import { useNetWork } from '/@/hooks/web/useNetWork';
@@ -48,7 +53,7 @@ export function useInitAppConfigStore() {
   if (!projCfg) {
   if (!projCfg) {
     projCfg = projectSetting;
     projCfg = projectSetting;
   }
   }
-  const { colorWeak, grayMode } = projCfg;
+  const { colorWeak, grayMode, headerBgColor, menuBgColor } = projCfg;
   try {
   try {
     // if (
     // if (
     //   themeColor !== primaryColor &&
     //   themeColor !== primaryColor &&
@@ -57,6 +62,8 @@ export function useInitAppConfigStore() {
     // ) {
     // ) {
     //   updateTheme(themeColor);
     //   updateTheme(themeColor);
     // }
     // }
+    headerBgColor && updateHeaderBgColor(headerBgColor);
+    menuBgColor && updateSidebarBgColor(menuBgColor);
     grayMode && updateGrayMode(grayMode);
     grayMode && updateGrayMode(grayMode);
     colorWeak && updateColorWeak(colorWeak);
     colorWeak && updateColorWeak(colorWeak);
   } catch (error) {
   } catch (error) {

+ 27 - 9
src/utils/color.ts

@@ -31,13 +31,31 @@ export const rgbToHex = function (r: number, g: number, b: number) {
  * @returns The RGB representation of the passed color
  * @returns The RGB representation of the passed color
  */
  */
 export const hexToRGB = function (hex: string) {
 export const hexToRGB = function (hex: string) {
-  return (
+  let sHex = hex.toLowerCase();
-    parseInt(hex.substring(0, 2), 16) +
+  if (isHexColor(hex)) {
-    ',' +
+    if (sHex.length === 4) {
-    parseInt(hex.substring(2, 4), 16) +
+      let sColorNew = '#';
-    ',' +
+      for (let i = 1; i < 4; i += 1) {
-    parseInt(hex.substring(4, 6), 16)
+        sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1));
-  );
+      }
+      sHex = sColorNew;
+    }
+    const sColorChange = [];
+    for (let i = 1; i < 7; i += 2) {
+      sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2)));
+    }
+    return 'RGB(' + sColorChange.join(',') + ')';
+  }
+  return sHex;
+};
+
+export const colorIsDark = (color: string) => {
+  if (!isHexColor(color)) return;
+  const [r, g, b] = hexToRGB(color)
+    .replace(/(?:\(|\)|rgb|RGB)*/g, '')
+    .split(',')
+    .map((item) => Number(item));
+  return r * 0.299 + g * 0.578 + b * 0.114 < 192;
 };
 };
 
 
 /**
 /**
@@ -89,7 +107,7 @@ const addLight = (color: string, amount: number) => {
  * @param {number} g green
  * @param {number} g green
  * @param {number} b blue
  * @param {number} b blue
  */
  */
-const luminanace = (r: stri, g: number, b: number) => {
+const luminanace = (r: number, g: number, b: number) => {
   const a = [r, g, b].map((v) => {
   const a = [r, g, b].map((v) => {
     v /= 255;
     v /= 255;
     return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
     return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
@@ -103,7 +121,7 @@ const luminanace = (r: stri, g: number, b: number) => {
  * @param {string} rgb2 rgb color 2
  * @param {string} rgb2 rgb color 2
  */
  */
 const contrast = (rgb1: string[], rgb2: number[]) =>
 const contrast = (rgb1: string[], rgb2: number[]) =>
-  (luminanace(rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) /
+  (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) /
   (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05);
   (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05);
 
 
 /**
 /**