Commit 97180e83f5055ebd138acc2a82c981d8a7399371

Authored by vben
1 parent 5091a875

feat(layout): added setting. Used to fix the left mixed mode menu

CHANGELOG.zh_CN.md
... ... @@ -3,6 +3,8 @@
3 3 ### ✨ Features
4 4  
5 5 - 新增`mixSideTrigger`配置。用于配置左侧混合模式菜单打开方式。可选`hover`,默认`click`
  6 +- 新增`mixSideFixed`配置。用于固定左侧混合模式菜单
  7 +- modal 组件新增`height`和`min-height`属性
6 8  
7 9 ### 🐛 Bug Fixes
8 10  
... ...
package.json
... ... @@ -7,7 +7,7 @@
7 7 "build": "cross-env vite build --mode=production && esno ./build/script/postBuild.ts",
8 8 "build:site": "cross-env SITE=true npm run build ",
9 9 "build:no-cache": "yarn clean:cache && npm run build",
10   - "typecheck": "typecheck .",
  10 + "typecheck": "vuedx-typecheck .",
11 11 "report": "cross-env REPORT=true npm run build ",
12 12 "preview": "npm run build && esno ./build/script/preview.ts",
13 13 "preview:dist": "esno ./build/script/preview.ts",
... ...
src/components/Menu/src/BasicMenu.vue
... ... @@ -125,15 +125,13 @@
125 125 }
126 126 });
127 127  
128   - watch(
129   - () => props.items,
130   - () => {
131   - handleMenuChange();
132   - }
133   - // {
134   - // immediate: true,
135   - // }
136   - );
  128 + !props.mixSider &&
  129 + watch(
  130 + () => props.items,
  131 + () => {
  132 + handleMenuChange();
  133 + }
  134 + );
137 135  
138 136 async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) {
139 137 const { beforeClickFn } = props;
... ...
src/components/Menu/src/useOpenKeys.ts
... ... @@ -8,6 +8,7 @@ import { unref } from 'vue';
8 8 import { es6Unique } from '/@/utils';
9 9 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
10 10 import { getAllParentPath } from '/@/router/helper/menuHelper';
  11 +import { useTimeoutFn } from '/@/hooks/core/useTimeout';
11 12  
12 13 export function useOpenKeys(
13 14 menuState: MenuState,
... ... @@ -15,18 +16,29 @@ export function useOpenKeys(
15 16 mode: Ref<MenuModeEnum>,
16 17 accordion: Ref<boolean>
17 18 ) {
18   - const { getCollapsed, getIsMixSidebar } = useMenuSetting();
  19 + const { getCollapsed, getIsMixSidebar, getMixSideFixed } = useMenuSetting();
19 20  
20   - function setOpenKeys(path: string) {
  21 + async function setOpenKeys(path: string) {
21 22 if (mode.value === MenuModeEnum.HORIZONTAL) {
22 23 return;
23 24 }
24   - const menuList = toRaw(menus.value);
25   - if (!unref(accordion)) {
26   - menuState.openKeys = es6Unique([...menuState.openKeys, ...getAllParentPath(menuList, path)]);
27   - } else {
28   - menuState.openKeys = getAllParentPath(menuList, path);
29   - }
  25 + const native = unref(getIsMixSidebar) && unref(getMixSideFixed);
  26 +
  27 + useTimeoutFn(
  28 + () => {
  29 + const menuList = toRaw(menus.value);
  30 + if (!unref(accordion)) {
  31 + menuState.openKeys = es6Unique([
  32 + ...menuState.openKeys,
  33 + ...getAllParentPath(menuList, path),
  34 + ]);
  35 + } else {
  36 + menuState.openKeys = getAllParentPath(menuList, path);
  37 + }
  38 + },
  39 + 16,
  40 + native
  41 + );
30 42 }
31 43  
32 44 const getOpenKeys = computed(() => {
... ...
src/components/Table/src/hooks/useDataSource.ts
... ... @@ -218,7 +218,7 @@ export function useDataSource(
218 218 onMounted(() => {
219 219 useTimeoutFn(() => {
220 220 unref(propsRef).immediate && fetch();
221   - }, 0);
  221 + }, 16);
222 222 });
223 223  
224 224 return {
... ...
src/hooks/core/useTimeout.ts
... ... @@ -3,20 +3,23 @@ import { tryOnUnmounted } from &#39;/@/utils/helper/vueHelper&#39;;
3 3  
4 4 import { isFunction } from '/@/utils/is';
5 5  
6   -export function useTimeoutFn(handle: Fn<any>, wait: number) {
  6 +export function useTimeoutFn(handle: Fn<any>, wait: number, native = false) {
7 7 if (!isFunction(handle)) {
8 8 throw new Error('handle is not Function!');
9 9 }
10 10  
11 11 const { readyRef, stop, start } = useTimeoutRef(wait);
12   -
13   - watch(
14   - readyRef,
15   - (maturity) => {
16   - maturity && handle();
17   - },
18   - { immediate: false }
19   - );
  12 + if (native) {
  13 + handle();
  14 + } else {
  15 + watch(
  16 + readyRef,
  17 + (maturity) => {
  18 + maturity && handle();
  19 + },
  20 + { immediate: false }
  21 + );
  22 + }
20 23 return { readyRef, stop, start };
21 24 }
22 25  
... ...
src/hooks/setting/useMenuSetting.ts
1 1 import type { MenuSetting } from '/@/types/config';
2 2  
3   -import { computed, unref } from 'vue';
  3 +import { computed, unref, ref } from 'vue';
4 4  
5 5 import { appStore } from '/@/store/modules/app';
6 6  
... ... @@ -8,6 +8,8 @@ import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from &#39;/@/enums/appE
8 8 import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
9 9 import { useFullContent } from '/@/hooks/web/useFullContent';
10 10  
  11 +const mixSideHasChildren = ref(false);
  12 +
11 13 // Get menu configuration
12 14 const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
13 15  
... ... @@ -39,6 +41,8 @@ const getCanDrag = computed(() =&gt; unref(getMenuSetting).canDrag);
39 41  
40 42 const getAccordion = computed(() => unref(getMenuSetting).accordion);
41 43  
  44 +const getMixSideFixed = computed(() => unref(getMenuSetting).mixSideFixed);
  45 +
42 46 const getTopMenuAlign = computed(() => unref(getMenuSetting).topMenuAlign);
43 47  
44 48 const getCloseMixSidebarOnChange = computed(() => unref(getMenuSetting).closeMixSidebarOnChange);
... ... @@ -87,7 +91,8 @@ const getCalcContentWidth = computed(() =&gt; {
87 91 unref(getIsTopMenu) || !unref(getShowMenu) || (unref(getSplit) && unref(getMenuHidden))
88 92 ? 0
89 93 : unref(getIsMixSidebar)
90   - ? SIDE_BAR_SHOW_TIT_MINI_WIDTH
  94 + ? SIDE_BAR_SHOW_TIT_MINI_WIDTH +
  95 + (unref(getMixSideFixed) && unref(mixSideHasChildren) ? unref(getRealWidth) : 0)
91 96 : unref(getRealWidth);
92 97  
93 98 return `calc(100% - ${unref(width)}px)`;
... ... @@ -148,5 +153,7 @@ export function useMenuSetting() {
148 153 getIsMixSidebar,
149 154 getCloseMixSidebarOnChange,
150 155 getMixSideTrigger,
  156 + getMixSideFixed,
  157 + mixSideHasChildren,
151 158 };
152 159 }
... ...
src/layouts/default/setting/SettingDrawer.tsx
... ... @@ -75,6 +75,7 @@ export default defineComponent({
75 75 getIsMixSidebar,
76 76 getCloseMixSidebarOnChange,
77 77 getMixSideTrigger,
  78 + getMixSideFixed,
78 79 } = useMenuSetting();
79 80  
80 81 const {
... ... @@ -110,6 +111,12 @@ export default defineComponent({
110 111 def={unref(getSplit)}
111 112 disabled={!unref(getShowMenuRef) || unref(getMenuType) !== MenuTypeEnum.MIX}
112 113 />
  114 + <SwitchItem
  115 + title={t('layout.setting.mixSidebarFixed')}
  116 + event={HandlerEnum.MENU_FIXED_MIX_SIDEBAR}
  117 + def={unref(getMixSideFixed)}
  118 + disabled={!unref(getIsMixSidebar)}
  119 + />
113 120  
114 121 <SwitchItem
115 122 title={t('layout.setting.closeMixSidebarOnChange')}
... ...
src/layouts/default/setting/enum.ts
... ... @@ -27,6 +27,7 @@ export enum HandlerEnum {
27 27 MENU_FIXED,
28 28 MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE,
29 29 MENU_TRIGGER_MIX_SIDEBAR,
  30 + MENU_FIXED_MIX_SIDEBAR,
30 31  
31 32 // header
32 33 HEADER_SHOW,
... ...
src/layouts/default/setting/handler.ts
... ... @@ -70,6 +70,9 @@ export function handler(event: HandlerEnum, value: any): DeepPartial&lt;ProjectConf
70 70 case HandlerEnum.MENU_TRIGGER_MIX_SIDEBAR:
71 71 return { menuSetting: { mixSideTrigger: value } };
72 72  
  73 + case HandlerEnum.MENU_FIXED_MIX_SIDEBAR:
  74 + return { menuSetting: { mixSideTrigger: value } };
  75 +
73 76 // ============transition==================
74 77 case HandlerEnum.OPEN_PAGE_LOADING:
75 78 appStore.commitPageLoadingState(false);
... ...
src/layouts/default/sider/MixSider.vue
1 1 <template>
2   - <div :class="`${prefixCls}-dom`" />
  2 + <div :class="`${prefixCls}-dom`" :style="getDomStyle" />
3 3  
4 4 <div
5 5 v-click-outside="handleClickOutside"
... ... @@ -27,7 +27,7 @@
27 27 v-bind="getItemEvents(item)"
28 28 >
29 29 <MenuTag :item="item" :showTitle="false" :isHorizontal="false" />
30   - <g-icon
  30 + <Icon
31 31 :class="`${prefixCls}-module__icon`"
32 32 :size="22"
33 33 :icon="item.meta && item.meta.icon"
... ... @@ -48,6 +48,14 @@
48 48 ]"
49 49 >
50 50 <span class="text"> {{ title }}</span>
  51 + <Icon
  52 + :size="16"
  53 + v-if="getMixSideFixed"
  54 + icon="ri:pushpin-2-fill"
  55 + class="pushpin"
  56 + @click="handleFixedMenu"
  57 + />
  58 + <Icon :size="16" v-else icon="ri:pushpin-2-line" class="pushpin" @click="handleFixedMenu" />
51 59 </div>
52 60 <ScrollContainer :class="`${prefixCls}-menu-list__content`">
53 61 <BasicMenu
... ... @@ -70,20 +78,23 @@
70 78 <script lang="ts">
71 79 import { defineComponent, onMounted, ref, computed, CSSProperties, unref } from 'vue';
72 80 import type { Menu } from '/@/router/types';
73   - import type { RouteLocationNormalized } from 'vue-router';
  81 + import { RouteLocationNormalized } from 'vue-router';
74 82 import { useDesign } from '/@/hooks/web/useDesign';
75 83 import { getShallowMenus, getChildrenMenus, getCurrentParentPath } from '/@/router/menus';
76 84 import { useI18n } from '/@/hooks/web/useI18n';
77 85 import { ScrollContainer } from '/@/components/Container';
  86 + import Icon from '/@/components/Icon';
78 87 import { AppLogo } from '/@/components/Application';
79 88 import { useGo } from '/@/hooks/web/usePage';
80 89 import { BasicMenu, MenuTag } from '/@/components/Menu';
81 90 import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
82 91 import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
83 92 import { useDragLine } from './useLayoutSider';
  93 + import { useGlobSetting } from '/@/hooks/setting';
  94 +
  95 + import { SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
84 96  
85 97 import clickOutside from '/@/directives/clickOutside';
86   - import { useGlobSetting } from '/@/hooks/setting';
87 98  
88 99 export default defineComponent({
89 100 name: 'LayoutMixSider',
... ... @@ -92,6 +103,7 @@
92 103 AppLogo,
93 104 BasicMenu,
94 105 MenuTag,
  106 + Icon,
95 107 },
96 108 directives: {
97 109 clickOutside,
... ... @@ -101,6 +113,7 @@
101 113 const activePath = ref('');
102 114 const chilrenMenus = ref<Menu[]>([]);
103 115 const openMenu = ref(false);
  116 + const isInit = ref(false);
104 117 const dragBarRef = ref<ElRef>(null);
105 118 const sideRef = ref<ElRef>(null);
106 119 const currentRoute = ref<Nullable<RouteLocationNormalized>>(null);
... ... @@ -114,7 +127,12 @@
114 127 getCloseMixSidebarOnChange,
115 128 getMenuTheme,
116 129 getMixSideTrigger,
  130 + getRealWidth,
  131 + getMixSideFixed,
  132 + mixSideHasChildren,
  133 + setMenuSetting,
117 134 } = useMenuSetting();
  135 +
118 136 const { title } = useGlobSetting();
119 137  
120 138 useDragLine(sideRef, dragBarRef, true);
... ... @@ -127,14 +145,41 @@
127 145 }
128 146 );
129 147  
  148 + const getIsFixed = computed(() => {
  149 + mixSideHasChildren.value = unref(chilrenMenus).length > 0;
  150 + const isFixed = unref(getMixSideFixed) && unref(mixSideHasChildren);
  151 + if (isFixed) {
  152 + openMenu.value = true;
  153 + }
  154 + return isFixed;
  155 + });
  156 +
  157 + const getDomStyle = computed(
  158 + (): CSSProperties => {
  159 + const fixedWidth = unref(getIsFixed) ? unref(getRealWidth) : 0;
  160 + const width = `${SIDE_BAR_SHOW_TIT_MINI_WIDTH + fixedWidth}px`;
  161 + return {
  162 + width,
  163 + maxWidth: width,
  164 + minWidth: width,
  165 + flex: `0 0 ${width}`,
  166 + };
  167 + }
  168 + );
  169 +
130 170 const getMenuEvents = computed(() => {
131   - return unref(getMixSideTrigger) === 'hover'
132   - ? {
133   - onMouseleave: () => {
134   - openMenu.value = false;
135   - },
136   - }
137   - : {};
  171 + // return unref(getMixSideTrigger) === 'hover'
  172 + // ? {
  173 + // onMouseleave: () => {
  174 + // closeMenu();
  175 + // },
  176 + // }
  177 + // : {};
  178 + return {
  179 + onMouseleave: () => {
  180 + closeMenu();
  181 + },
  182 + };
138 183 });
139 184  
140 185 const getShowDragBar = computed(() => unref(getCanDrag));
... ... @@ -145,9 +190,9 @@
145 190  
146 191 listenerLastChangeTab((route) => {
147 192 currentRoute.value = route;
148   - setActive();
  193 + setActive(true);
149 194 if (unref(getCloseMixSidebarOnChange)) {
150   - openMenu.value = false;
  195 + closeMenu();
151 196 }
152 197 });
153 198  
... ... @@ -156,7 +201,11 @@
156 201  
157 202 if (unref(activePath) === path) {
158 203 if (!hover) {
159   - openMenu.value = !unref(openMenu);
  204 + if (!unref(openMenu)) {
  205 + openMenu.value = true;
  206 + } else {
  207 + closeMenu();
  208 + }
160 209 }
161 210 if (!unref(openMenu)) {
162 211 setActive();
... ... @@ -169,18 +218,32 @@
169 218 if (!children || children.length === 0) {
170 219 go(path);
171 220 chilrenMenus.value = [];
172   - openMenu.value = false;
  221 + closeMenu();
173 222 return;
174 223 }
175 224 chilrenMenus.value = children;
176 225 }
177 226  
178   - async function setActive() {
  227 + async function setActive(setChildren = false) {
179 228 const path = currentRoute.value?.path;
180 229 if (!path) return;
181 230 const parentPath = await getCurrentParentPath(path);
182 231 activePath.value = parentPath;
183 232 // hanldeModuleClick(parentPath);
  233 + if (unref(getMixSideFixed)) {
  234 + const activeMenu = unref(menuModules).find((item) => item.path === unref(activePath));
  235 + const p = activeMenu?.path;
  236 + if (p) {
  237 + const children = await getChildrenMenus(p);
  238 + if (setChildren) {
  239 + chilrenMenus.value = children;
  240 + openMenu.value = children.length > 0;
  241 + }
  242 + if (children.length === 0) {
  243 + chilrenMenus.value = [];
  244 + }
  245 + }
  246 + }
184 247 }
185 248  
186 249 function handleMenuClick(path: string) {
... ... @@ -188,7 +251,7 @@
188 251 }
189 252  
190 253 function handleClickOutside() {
191   - openMenu.value = false;
  254 + closeMenu();
192 255 setActive();
193 256 }
194 257  
... ... @@ -203,6 +266,18 @@
203 266 };
204 267 }
205 268  
  269 + function handleFixedMenu() {
  270 + setMenuSetting({
  271 + mixSideFixed: !unref(getIsFixed),
  272 + });
  273 + }
  274 +
  275 + function closeMenu() {
  276 + if (!unref(getIsFixed)) {
  277 + openMenu.value = false;
  278 + }
  279 + }
  280 +
206 281 return {
207 282 t,
208 283 prefixCls,
... ... @@ -221,6 +296,9 @@
221 296 getMenuTheme,
222 297 getItemEvents,
223 298 getMenuEvents,
  299 + getDomStyle,
  300 + handleFixedMenu,
  301 + getMixSideFixed,
224 302 };
225 303 },
226 304 });
... ... @@ -241,7 +319,7 @@
241 319 min-width: @width;
242 320 overflow: hidden;
243 321 background: @sider-dark-bg-color;
244   - transition: all 0.2s ease 0s;
  322 + transition: all 0.3s ease 0s;
245 323 flex: 0 0 @width;
246 324 .@{tag-prefix-cls} {
247 325 position: absolute;
... ... @@ -293,6 +371,17 @@
293 371 }
294 372 }
295 373 }
  374 + .@{prefix-cls}-menu-list {
  375 + &__title {
  376 + .pushpin {
  377 + color: rgba(0, 0, 0, 0.35);
  378 +
  379 + &:hover {
  380 + color: rgba(0, 0, 0, 0.85);
  381 + }
  382 + }
  383 + }
  384 + }
296 385 }
297 386 @border-color: @sider-dark-lighten-1-bg-color;
298 387  
... ... @@ -388,20 +477,30 @@
388 477 &__title {
389 478 display: flex;
390 479 height: @header-height;
391   - margin-left: -6px;
  480 + // margin-left: -6px;
392 481 font-size: 18px;
393 482 color: @primary-color;
394 483 border-bottom: 1px solid rgb(238, 238, 238);
395 484 opacity: 0;
396 485 transition: unset;
397   - // justify-content: center;
398 486 align-items: center;
399   - justify-content: start;
  487 + justify-content: space-between;
400 488  
401 489 &.show {
  490 + min-width: 130px;
402 491 opacity: 1;
403 492 transition: all 0.5s ease;
404 493 }
  494 +
  495 + .pushpin {
  496 + margin-right: 6px;
  497 + color: rgba(255, 255, 255, 0.65);
  498 + cursor: pointer;
  499 +
  500 + &:hover {
  501 + color: #fff;
  502 + }
  503 + }
405 504 }
406 505  
407 506 &__content {
... ...
src/locales/lang/en/layout/setting.ts
... ... @@ -77,4 +77,6 @@ export default {
77 77 mixSidebarTrigger: 'Mixed menu Trigger',
78 78 triggerHover: 'Hover',
79 79 triggerClick: 'Click',
  80 +
  81 + mixSidebarFixed: 'Fixed expanded menu',
80 82 };
... ...
src/locales/lang/zh_CN/layout/setting.ts
... ... @@ -76,4 +76,6 @@ export default {
76 76 mixSidebarTrigger: '混合菜单触发方式',
77 77 triggerHover: '悬停',
78 78 triggerClick: '点击',
  79 +
  80 + mixSidebarFixed: '固定展开菜单',
79 81 };
... ...
src/settings/projectSetting.ts
... ... @@ -110,6 +110,8 @@ const setting: ProjectConfig = {
110 110 closeMixSidebarOnChange: false,
111 111 // Module opening method ‘click’ |'hover'
112 112 mixSideTrigger: MixSidebarTriggerEnum.CLICK,
  113 + // Fixed expanded menu
  114 + mixSideFixed: false,
113 115 },
114 116  
115 117 // Multi-label
... ...
src/types/config.d.ts
... ... @@ -21,6 +21,7 @@ export interface MenuSetting {
21 21 closeMixSidebarOnChange: boolean;
22 22 collapsedShowTitle: boolean;
23 23 mixSideTrigger: MixSidebarTriggerEnum;
  24 + mixSideFixed: boolean;
24 25 }
25 26  
26 27 export interface MultiTabsSetting {
... ...
tsconfig.json
1 1 {
2 2 "compilerOptions": {
3   - "target": "es2016",
  3 + "target": "esnext",
4 4 "module": "esnext",
5 5 "moduleResolution": "node",
6 6 "strict": true,
7 7 "forceConsistentCasingInFileNames": true,
8 8 "allowSyntheticDefaultImports": true,
9 9 "strictFunctionTypes": false,
10   - "jsx": "react",
  10 + "jsx": "preserve",
11 11 "baseUrl": ".",
12 12 "allowJs": true,
13 13 "sourceMap": true,
... ...