Commit 97180e83f5055ebd138acc2a82c981d8a7399371
1 parent
5091a875
feat(layout): added setting. Used to fix the left mixed mode menu
Showing
16 changed files
with
192 additions
and
53 deletions
CHANGELOG.zh_CN.md
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
src/hooks/core/useTimeout.ts
... | ... | @@ -3,20 +3,23 @@ import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; |
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 '/@/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(() => 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(() => { |
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
src/layouts/default/setting/handler.ts
... | ... | @@ -70,6 +70,9 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<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
src/locales/lang/zh_CN/layout/setting.ts
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
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, | ... | ... |