Commit f645680a3b9a1f75395329970551d9e5d6bd845b

Authored by vben
1 parent c8021ef3

feat: right-click menu supports multiple levels

CHANGELOG.zh_CN.md
... ... @@ -3,6 +3,7 @@
3 3 ### ✨ Features
4 4  
5 5 - 全局 loading 添加文本
  6 +- 右键菜单支持多级
6 7  
7 8 ### 🎫 Chores
8 9  
... ... @@ -13,7 +14,7 @@
13 14 - Layout 界面布局样式调整
14 15 - 优化表格渲染性能
15 16 - 表单折叠搜索添图标添加动画
16   -- routeModule 可以忽略 layou 配置不写。方便配置一级菜单
  17 +- routeModule 可以忽略 layout 配置不写。方便配置一级菜单
17 18  
18 19 ### 🐛 Bug Fixes
19 20  
... ...
src/components/ContextMenu/src/index.less
1 1 @import (reference) '../../../design/index.less';
2 2  
  3 +.item-style() {
  4 + li {
  5 + display: inline-block;
  6 + width: 100%;
  7 + height: 46px !important;
  8 + margin: 0 !important;
  9 + line-height: 46px;
  10 +
  11 + span {
  12 + line-height: 46px;
  13 + }
  14 +
  15 + > div {
  16 + margin: 0 !important;
  17 + }
  18 +
  19 + &:hover {
  20 + color: @text-color-base;
  21 + background: #eee;
  22 + }
  23 + }
  24 +}
  25 +
3 26 .context-menu {
4 27 position: fixed;
5 28 top: 0;
... ... @@ -18,32 +41,17 @@
18 41 background-clip: padding-box;
19 42 user-select: none;
20 43  
21   - &.hidden {
22   - display: none !important;
23   - }
  44 + .item-style();
24 45  
25   - &__item {
26   - a {
27   - display: inline-block;
28   - width: 100%;
29   - padding: 10px 14px;
  46 + .ant-divider {
  47 + margin: 0 0;
  48 + }
30 49  
31   - &:hover {
32   - color: @text-color-base;
33   - background: #eee;
34   - }
  50 + &__popup {
  51 + .ant-divider {
  52 + margin: 0 0;
35 53 }
36 54  
37   - &.disabled {
38   - a {
39   - color: @disabled-color;
40   - cursor: not-allowed;
41   -
42   - &:hover {
43   - color: @disabled-color;
44   - background: unset;
45   - }
46   - }
47   - }
  55 + .item-style();
48 56 }
49 57 }
... ...
src/components/ContextMenu/src/index.tsx
... ... @@ -8,9 +8,13 @@ import {
8 8 unref,
9 9 onUnmounted,
10 10 } from 'vue';
  11 +
11 12 import { props } from './props';
12 13 import Icon from '/@/components/Icon';
  14 +import { Menu, Divider } from 'ant-design-vue';
  15 +
13 16 import type { ContextMenuItem } from './types';
  17 +
14 18 import './index.less';
15 19 const prefixCls = 'context-menu';
16 20 export default defineComponent({
... ... @@ -43,12 +47,13 @@ export default defineComponent({
43 47 top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
44 48 };
45 49 });
  50 +
46 51 function handleAction(item: ContextMenuItem, e: MouseEvent) {
  52 + state.show = false;
47 53 const { handler, disabled } = item;
48 54 if (disabled) {
49 55 return;
50 56 }
51   - state.show = false;
52 57 if (e) {
53 58 e.stopPropagation();
54 59 e.preventDefault();
... ... @@ -61,31 +66,47 @@ export default defineComponent({
61 66  
62 67 const { showIcon } = props;
63 68 return (
64   - <span style="display: inline-block; width: 100%;">
  69 + <span style="display: inline-block; width: 100%;" onClick={handleAction.bind(null, item)}>
65 70 {showIcon && icon && <Icon class="mr-2" icon={icon} />}
66 71 <span>{label}</span>
67 72 </span>
68 73 );
69 74 }
70 75 function renderMenuItem(items: ContextMenuItem[]) {
71   - return items.map((item) => {
72   - const { disabled, label } = item;
  76 + return items.map((item, index) => {
  77 + const { disabled, label, children, divider = false } = item;
73 78  
74   - return (
75   - <li class={`${prefixCls}__item ${disabled ? 'disabled' : ''}`} key={label}>
76   - <a onClick={handleAction.bind(null, item)} style="color:#333;">
77   - {renderContent(item)}
78   - </a>
79   - </li>
  79 + const DividerComp = divider ? <Divider key={`d-${index}`} /> : null;
  80 + if (!children || children.length === 0) {
  81 + return [
  82 + <Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
  83 + {() => [renderContent(item)]}
  84 + </Menu.Item>,
  85 + DividerComp,
  86 + ];
  87 + }
  88 + return !state.show ? null : (
  89 + <Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup `}>
  90 + {{
  91 + title: () => renderContent(item),
  92 + default: () => [renderMenuItem(children)],
  93 + }}
  94 + </Menu.SubMenu>
80 95 );
81 96 });
82 97 }
83 98 return () => {
84 99 const { items } = props;
85   - return (
86   - <ul class={[prefixCls, !state.show && 'hidden']} ref={wrapRef} style={unref(getStyle)}>
87   - {renderMenuItem(items)}
88   - </ul>
  100 + return !state.show ? null : (
  101 + <Menu
  102 + inlineIndent={12}
  103 + mode="vertical"
  104 + class={[prefixCls]}
  105 + ref={wrapRef}
  106 + style={unref(getStyle)}
  107 + >
  108 + {() => renderMenuItem(items)}
  109 + </Menu>
89 110 );
90 111 };
91 112 },
... ...
src/components/Description/src/index.tsx
... ... @@ -23,6 +23,7 @@ export default defineComponent({
23 23 ...unref(propsRef),
24 24 };
25 25 });
  26 +
26 27 const getProps = computed(() => {
27 28 const opt = {
28 29 ...props,
... ... @@ -31,12 +32,14 @@ export default defineComponent({
31 32 };
32 33 return opt;
33 34 });
  35 +
34 36 /**
35 37 * @description: 是否使用标题
36 38 */
37 39 const useWrapper = computed(() => {
38 40 return !!unref(getMergeProps).title;
39 41 });
  42 +
40 43 /**
41 44 * @description: 获取配置Collapse
42 45 */
... ... @@ -49,6 +52,7 @@ export default defineComponent({
49 52 };
50 53 }
51 54 );
  55 +
52 56 /**
53 57 * @description:设置desc
54 58 */
... ... @@ -57,9 +61,11 @@ export default defineComponent({
57 61 const mergeProps = deepMerge(unref(propsRef) || {}, descProps);
58 62 propsRef.value = cloneDeep(mergeProps);
59 63 }
  64 +
60 65 const methods: DescInstance = {
61 66 setDescProps,
62 67 };
  68 +
63 69 emit('register', methods);
64 70  
65 71 // 防止换行
... ... @@ -95,6 +101,7 @@ export default defineComponent({
95 101  
96 102 const width = contentMinWidth;
97 103 return (
  104 + // @ts-ignore
98 105 <Descriptions.Item label={renderLabel(item)} key={field} span={span}>
99 106 {() =>
100 107 contentMinWidth ? (
... ... @@ -113,13 +120,15 @@ export default defineComponent({
113 120 );
114 121 });
115 122 }
  123 +
116 124 const renderDesc = () => {
117 125 return (
118   - <Descriptions class={`${prefixCls}`} {...{ ...attrs, ...unref(getProps) }}>
  126 + <Descriptions class={`${prefixCls}`} {...{ ...attrs, ...(unref(getProps) as any) }}>
119 127 {() => renderItem()}
120 128 </Descriptions>
121 129 );
122 130 };
  131 +
123 132 const renderContainer = () => {
124 133 const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>;
125 134 // 减少dom层级
... ...
src/components/Description/src/useDescription.ts
... ... @@ -10,7 +10,7 @@ export function useDescription(props?: Partial&lt;DescOptions&gt;): UseDescReturnType
10 10 const descRef = ref<DescInstance | null>(null);
11 11 const loadedRef = ref(false);
12 12  
13   - function getDescription(instance: DescInstance) {
  13 + function register(instance: DescInstance) {
14 14 if (unref(loadedRef) && isProdMode()) {
15 15 return;
16 16 }
... ... @@ -18,10 +18,11 @@ export function useDescription(props?: Partial&lt;DescOptions&gt;): UseDescReturnType
18 18 props && instance.setDescProps(props);
19 19 loadedRef.value = true;
20 20 }
  21 +
21 22 const methods: DescInstance = {
22 23 setDescProps: (descProps: Partial<DescOptions>): void => {
23 24 unref(descRef)!.setDescProps(descProps);
24 25 },
25 26 };
26   - return [getDescription, methods];
  27 + return [register, methods];
27 28 }
... ...
src/components/Icon/index.tsx
... ... @@ -32,6 +32,7 @@ export default defineComponent({
32 32 const { icon, prefix } = props;
33 33 return `${prefix ? prefix + ':' : ''}${icon}`;
34 34 });
  35 +
35 36 const update = async () => {
36 37 const el = unref(elRef);
37 38 if (el) {
... ... @@ -67,6 +68,7 @@ export default defineComponent({
67 68 });
68 69  
69 70 watch(() => props.icon, update, { flush: 'post' });
  71 +
70 72 onMounted(update);
71 73  
72 74 return () => (
... ...
src/components/Menu/src/BasicMenu.tsx
... ... @@ -55,6 +55,7 @@ export default defineComponent({
55 55 }
56 56 return menuState.openKeys;
57 57 });
  58 +
58 59 // menu外层样式
59 60 const getMenuWrapStyle = computed((): any => {
60 61 const { showLogo, search } = props;
... ... @@ -130,6 +131,7 @@ export default defineComponent({
130 131 menuState.selectedKeys = [path];
131 132 emit('menuClick', menu);
132 133 }
  134 +
133 135 function handleMenuChange() {
134 136 const { flatItems } = props;
135 137 if (!unref(flatItems) || flatItems.length === 0) {
... ...
src/components/Menu/src/useSearchInput.ts
... ... @@ -48,9 +48,11 @@ export function useSearchInput({
48 48 openKeys = es6Unique(openKeys);
49 49 menuState.openKeys = openKeys;
50 50 }
  51 +
51 52 // 搜索框点击
52 53 function handleInputClick(e: any): void {
53 54 emit('clickSearchInput', e);
54 55 }
  56 +
55 57 return { handleInputChange, handleInputClick };
56 58 }
... ...
src/components/Preview/src/index.tsx
... ... @@ -219,6 +219,7 @@ export default defineComponent({
219 219 </div>
220 220 );
221 221 };
  222 +
222 223 const renderIndex = () => {
223 224 if (!unref(getIsMultipleImage)) {
224 225 return null;
... ...
src/hooks/web/useWatermark.ts
... ... @@ -3,6 +3,7 @@ import { getCurrentInstance, onBeforeUnmount, ref, Ref, unref } from &#39;vue&#39;;
3 3 const domSymbol = Symbol('watermark-dom');
4 4  
5 5 export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.body)) {
  6 + let func: Fn = () => {};
6 7 const id = domSymbol.toString();
7 8 const clear = () => {
8 9 const domId = document.getElementById(id);
... ... @@ -10,6 +11,7 @@ export function useWatermark(appendEl: Ref&lt;HTMLElement | null&gt; = ref(document.bo
10 11 const el = unref(appendEl);
11 12 el && el.removeChild(domId);
12 13 }
  14 + window.addEventListener('resize', func);
13 15 };
14 16 const createWatermark = (str: string) => {
15 17 clear();
... ... @@ -45,7 +47,7 @@ export function useWatermark(appendEl: Ref&lt;HTMLElement | null&gt; = ref(document.bo
45 47  
46 48 function setWatermark(str: string) {
47 49 createWatermark(str);
48   - const func = () => {
  50 + func = () => {
49 51 createWatermark(str);
50 52 };
51 53 window.addEventListener('resize', func);
... ... @@ -53,7 +55,6 @@ export function useWatermark(appendEl: Ref&lt;HTMLElement | null&gt; = ref(document.bo
53 55 if (instance) {
54 56 onBeforeUnmount(() => {
55 57 clear();
56   - window.addEventListener('resize', func);
57 58 });
58 59 }
59 60 }
... ...
src/router/routes/modules/demo/feat.ts
... ... @@ -18,7 +18,7 @@ export default {
18 18 {
19 19 path: '/icon',
20 20 name: 'IconDemo',
21   - component: () => import('/@/views/demo/comp/icon/index.vue'),
  21 + component: () => import('/@/views/demo/feat/icon/index.vue'),
22 22 meta: {
23 23 title: '图标',
24 24 },
... ... @@ -43,7 +43,7 @@ export default {
43 43 {
44 44 path: '/click-out-side',
45 45 name: 'ClickOutSideDemo',
46   - component: () => import('/@/views/demo/comp/click-out-side/index.vue'),
  46 + component: () => import('/@/views/demo/feat/click-out-side/index.vue'),
47 47 meta: {
48 48 title: 'ClickOutSide组件',
49 49 },
... ...
src/views/demo/comp/button/index.vue
... ... @@ -7,8 +7,6 @@
7 7 show-icon
8 8 />
9 9  
10   - <Alert message="按钮扩展" type="info" show-icon class="mt-4" />
11   -
12 10 <div class="my-2">
13 11 <h3>success</h3>
14 12 <a-button color="success">成功</a-button>
... ...
src/views/demo/comp/click-out-side/index.vue renamed to src/views/demo/feat/click-out-side/index.vue
1 1 <template>
2   - <div class="px-10">
3   - <Alert message="点内外部触发事件" show-icon class="mt-4"></Alert>
  2 + <div class="p-10">
  3 + <Alert message="点内外部触发事件" show-icon></Alert>
4 4 <ClickOutSide @clickOutside="handleClickOutside" class="flex justify-center mt-10">
5 5 <div @click="innerClick" class="demo-box">
6 6 {{ text }}
... ...
src/views/demo/feat/context-menu/index.vue
... ... @@ -3,6 +3,10 @@
3 3 <CollapseContainer title="Simple">
4 4 <a-button type="primary" @contextmenu="handleContext">Right Click on me</a-button>
5 5 </CollapseContainer>
  6 +
  7 + <CollapseContainer title="Multiple" class="mt-4">
  8 + <a-button type="primary" @contextmenu="handleMultipleContext">Right Click on me</a-button>
  9 + </CollapseContainer>
6 10 </div>
7 11 </template>
8 12 <script lang="ts">
... ... @@ -36,7 +40,44 @@
36 40 ],
37 41 });
38 42 }
39   - return { handleContext };
  43 +
  44 + function handleMultipleContext(e: MouseEvent) {
  45 + createContextMenu({
  46 + event: e,
  47 + items: [
  48 + {
  49 + label: 'New',
  50 + icon: 'ant-design:plus-outlined',
  51 +
  52 + children: [
  53 + {
  54 + label: 'New1-1',
  55 + icon: 'ant-design:plus-outlined',
  56 + divider: true,
  57 + children: [
  58 + {
  59 + label: 'New1-1-1',
  60 + handler: () => {
  61 + createMessage.success('click new');
  62 + },
  63 + },
  64 + {
  65 + label: 'New1-2-1',
  66 + disabled: true,
  67 + },
  68 + ],
  69 + },
  70 + {
  71 + label: 'New1-2',
  72 + icon: 'ant-design:plus-outlined',
  73 + },
  74 + ],
  75 + },
  76 + ],
  77 + });
  78 + }
  79 +
  80 + return { handleContext, handleMultipleContext };
40 81 },
41 82 });
42 83 </script>
... ...
src/views/demo/comp/icon/index.vue renamed to src/views/demo/feat/icon/index.vue
... ... @@ -12,7 +12,7 @@
12 12 </div>
13 13 </CollapseContainer>
14 14  
15   - <CollapseContainer title="IconIfy 组件使用" class="mt-5">
  15 + <CollapseContainer title="IconIfy 组件使用" class="my-5">
16 16 <div class="flex justify-around flex-wrap">
17 17 <Icon icon="fa-solid:address-book" :size="30" />
18 18 <Icon icon="mdi-light:bank" :size="30" />
... ... @@ -23,7 +23,6 @@
23 23  
24 24 <Alert
25 25 show-icon
26   - class="mt-5"
27 26 message="推荐使用Iconify组件"
28 27 description="Icon组件基本包含所有的图标,在下面网址内你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。唯一不足的可能就是需要连接外网进行使用。"
29 28 />
... ...