Commit f645680a3b9a1f75395329970551d9e5d6bd845b

Authored by vben
1 parent c8021ef3

feat: right-click menu supports multiple levels

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