提交 f645680a 编写于 作者: V vben

feat: right-click menu supports multiple levels

上级 c8021ef3
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
### ✨ Features ### ✨ Features
- 全局 loading 添加文本 - 全局 loading 添加文本
- 右键菜单支持多级
### 🎫 Chores ### 🎫 Chores
...@@ -13,7 +14,7 @@ ...@@ -13,7 +14,7 @@
- Layout 界面布局样式调整 - Layout 界面布局样式调整
- 优化表格渲染性能 - 优化表格渲染性能
- 表单折叠搜索添图标添加动画 - 表单折叠搜索添图标添加动画
- routeModule 可以忽略 layou 配置不写。方便配置一级菜单 - routeModule 可以忽略 layout 配置不写。方便配置一级菜单
### 🐛 Bug Fixes ### 🐛 Bug Fixes
......
@import (reference) '../../../design/index.less'; @import (reference) '../../../design/index.less';
.item-style() {
li {
display: inline-block;
width: 100%;
height: 46px !important;
margin: 0 !important;
line-height: 46px;
span {
line-height: 46px;
}
> div {
margin: 0 !important;
}
&:hover {
color: @text-color-base;
background: #eee;
}
}
}
.context-menu { .context-menu {
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -18,32 +41,17 @@ ...@@ -18,32 +41,17 @@
background-clip: padding-box; background-clip: padding-box;
user-select: none; user-select: none;
&.hidden { .item-style();
display: none !important;
}
&__item { .ant-divider {
a { margin: 0 0;
display: inline-block; }
width: 100%;
padding: 10px 14px;
&:hover { &__popup {
color: @text-color-base; .ant-divider {
background: #eee; margin: 0 0;
}
} }
&.disabled { .item-style();
a {
color: @disabled-color;
cursor: not-allowed;
&:hover {
color: @disabled-color;
background: unset;
}
}
}
} }
} }
...@@ -8,9 +8,13 @@ import { ...@@ -8,9 +8,13 @@ import {
unref, unref,
onUnmounted, onUnmounted,
} from 'vue'; } from 'vue';
import { props } from './props'; import { props } from './props';
import Icon from '/@/components/Icon'; import Icon from '/@/components/Icon';
import { Menu, Divider } from 'ant-design-vue';
import type { ContextMenuItem } from './types'; import type { ContextMenuItem } from './types';
import './index.less'; import './index.less';
const prefixCls = 'context-menu'; const prefixCls = 'context-menu';
export default defineComponent({ export default defineComponent({
...@@ -43,12 +47,13 @@ export default defineComponent({ ...@@ -43,12 +47,13 @@ export default defineComponent({
top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px', top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
}; };
}); });
function handleAction(item: ContextMenuItem, e: MouseEvent) { function handleAction(item: ContextMenuItem, e: MouseEvent) {
state.show = false;
const { handler, disabled } = item; const { handler, disabled } = item;
if (disabled) { if (disabled) {
return; return;
} }
state.show = false;
if (e) { if (e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
...@@ -61,31 +66,47 @@ export default defineComponent({ ...@@ -61,31 +66,47 @@ export default defineComponent({
const { showIcon } = props; const { showIcon } = props;
return ( return (
<span style="display: inline-block; width: 100%;"> <span style="display: inline-block; width: 100%;" onClick={handleAction.bind(null, item)}>
{showIcon && icon && <Icon class="mr-2" icon={icon} />} {showIcon && icon && <Icon class="mr-2" icon={icon} />}
<span>{label}</span> <span>{label}</span>
</span> </span>
); );
} }
function renderMenuItem(items: ContextMenuItem[]) { function renderMenuItem(items: ContextMenuItem[]) {
return items.map((item) => { return items.map((item, index) => {
const { disabled, label } = item; const { disabled, label, children, divider = false } = item;
return ( const DividerComp = divider ? <Divider key={`d-${index}`} /> : null;
<li class={`${prefixCls}__item ${disabled ? 'disabled' : ''}`} key={label}> if (!children || children.length === 0) {
<a onClick={handleAction.bind(null, item)} style="color:#333;"> return [
{renderContent(item)} <Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
</a> {() => [renderContent(item)]}
</li> </Menu.Item>,
DividerComp,
];
}
return !state.show ? null : (
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup `}>
{{
title: () => renderContent(item),
default: () => [renderMenuItem(children)],
}}
</Menu.SubMenu>
); );
}); });
} }
return () => { return () => {
const { items } = props; const { items } = props;
return ( return !state.show ? null : (
<ul class={[prefixCls, !state.show && 'hidden']} ref={wrapRef} style={unref(getStyle)}> <Menu
{renderMenuItem(items)} inlineIndent={12}
</ul> mode="vertical"
class={[prefixCls]}
ref={wrapRef}
style={unref(getStyle)}
>
{() => renderMenuItem(items)}
</Menu>
); );
}; };
}, },
......
...@@ -23,6 +23,7 @@ export default defineComponent({ ...@@ -23,6 +23,7 @@ export default defineComponent({
...unref(propsRef), ...unref(propsRef),
}; };
}); });
const getProps = computed(() => { const getProps = computed(() => {
const opt = { const opt = {
...props, ...props,
...@@ -31,12 +32,14 @@ export default defineComponent({ ...@@ -31,12 +32,14 @@ export default defineComponent({
}; };
return opt; return opt;
}); });
/** /**
* @description: 是否使用标题 * @description: 是否使用标题
*/ */
const useWrapper = computed(() => { const useWrapper = computed(() => {
return !!unref(getMergeProps).title; return !!unref(getMergeProps).title;
}); });
/** /**
* @description: 获取配置Collapse * @description: 获取配置Collapse
*/ */
...@@ -49,6 +52,7 @@ export default defineComponent({ ...@@ -49,6 +52,7 @@ export default defineComponent({
}; };
} }
); );
/** /**
* @description:设置desc * @description:设置desc
*/ */
...@@ -57,9 +61,11 @@ export default defineComponent({ ...@@ -57,9 +61,11 @@ export default defineComponent({
const mergeProps = deepMerge(unref(propsRef) || {}, descProps); const mergeProps = deepMerge(unref(propsRef) || {}, descProps);
propsRef.value = cloneDeep(mergeProps); propsRef.value = cloneDeep(mergeProps);
} }
const methods: DescInstance = { const methods: DescInstance = {
setDescProps, setDescProps,
}; };
emit('register', methods); emit('register', methods);
// 防止换行 // 防止换行
...@@ -95,6 +101,7 @@ export default defineComponent({ ...@@ -95,6 +101,7 @@ export default defineComponent({
const width = contentMinWidth; const width = contentMinWidth;
return ( return (
// @ts-ignore
<Descriptions.Item label={renderLabel(item)} key={field} span={span}> <Descriptions.Item label={renderLabel(item)} key={field} span={span}>
{() => {() =>
contentMinWidth ? ( contentMinWidth ? (
...@@ -113,13 +120,15 @@ export default defineComponent({ ...@@ -113,13 +120,15 @@ export default defineComponent({
); );
}); });
} }
const renderDesc = () => { const renderDesc = () => {
return ( return (
<Descriptions class={`${prefixCls}`} {...{ ...attrs, ...unref(getProps) }}> <Descriptions class={`${prefixCls}`} {...{ ...attrs, ...(unref(getProps) as any) }}>
{() => renderItem()} {() => renderItem()}
</Descriptions> </Descriptions>
); );
}; };
const renderContainer = () => { const renderContainer = () => {
const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>; const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>;
// 减少dom层级 // 减少dom层级
......
...@@ -10,7 +10,7 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType ...@@ -10,7 +10,7 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType
const descRef = ref<DescInstance | null>(null); const descRef = ref<DescInstance | null>(null);
const loadedRef = ref(false); const loadedRef = ref(false);
function getDescription(instance: DescInstance) { function register(instance: DescInstance) {
if (unref(loadedRef) && isProdMode()) { if (unref(loadedRef) && isProdMode()) {
return; return;
} }
...@@ -18,10 +18,11 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType ...@@ -18,10 +18,11 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType
props && instance.setDescProps(props); props && instance.setDescProps(props);
loadedRef.value = true; loadedRef.value = true;
} }
const methods: DescInstance = { const methods: DescInstance = {
setDescProps: (descProps: Partial<DescOptions>): void => { setDescProps: (descProps: Partial<DescOptions>): void => {
unref(descRef)!.setDescProps(descProps); unref(descRef)!.setDescProps(descProps);
}, },
}; };
return [getDescription, methods]; return [register, methods];
} }
...@@ -32,6 +32,7 @@ export default defineComponent({ ...@@ -32,6 +32,7 @@ export default defineComponent({
const { icon, prefix } = props; const { icon, prefix } = props;
return `${prefix ? prefix + ':' : ''}${icon}`; return `${prefix ? prefix + ':' : ''}${icon}`;
}); });
const update = async () => { const update = async () => {
const el = unref(elRef); const el = unref(elRef);
if (el) { if (el) {
...@@ -67,6 +68,7 @@ export default defineComponent({ ...@@ -67,6 +68,7 @@ export default defineComponent({
}); });
watch(() => props.icon, update, { flush: 'post' }); watch(() => props.icon, update, { flush: 'post' });
onMounted(update); onMounted(update);
return () => ( return () => (
......
...@@ -55,6 +55,7 @@ export default defineComponent({ ...@@ -55,6 +55,7 @@ export default defineComponent({
} }
return menuState.openKeys; return menuState.openKeys;
}); });
// menu外层样式 // menu外层样式
const getMenuWrapStyle = computed((): any => { const getMenuWrapStyle = computed((): any => {
const { showLogo, search } = props; const { showLogo, search } = props;
...@@ -130,6 +131,7 @@ export default defineComponent({ ...@@ -130,6 +131,7 @@ export default defineComponent({
menuState.selectedKeys = [path]; menuState.selectedKeys = [path];
emit('menuClick', menu); emit('menuClick', menu);
} }
function handleMenuChange() { function handleMenuChange() {
const { flatItems } = props; const { flatItems } = props;
if (!unref(flatItems) || flatItems.length === 0) { if (!unref(flatItems) || flatItems.length === 0) {
......
...@@ -48,9 +48,11 @@ export function useSearchInput({ ...@@ -48,9 +48,11 @@ export function useSearchInput({
openKeys = es6Unique(openKeys); openKeys = es6Unique(openKeys);
menuState.openKeys = openKeys; menuState.openKeys = openKeys;
} }
// 搜索框点击 // 搜索框点击
function handleInputClick(e: any): void { function handleInputClick(e: any): void {
emit('clickSearchInput', e); emit('clickSearchInput', e);
} }
return { handleInputChange, handleInputClick }; return { handleInputChange, handleInputClick };
} }
...@@ -219,6 +219,7 @@ export default defineComponent({ ...@@ -219,6 +219,7 @@ export default defineComponent({
</div> </div>
); );
}; };
const renderIndex = () => { const renderIndex = () => {
if (!unref(getIsMultipleImage)) { if (!unref(getIsMultipleImage)) {
return null; return null;
......
...@@ -3,6 +3,7 @@ import { getCurrentInstance, onBeforeUnmount, ref, Ref, unref } from 'vue'; ...@@ -3,6 +3,7 @@ import { getCurrentInstance, onBeforeUnmount, ref, Ref, unref } from 'vue';
const domSymbol = Symbol('watermark-dom'); const domSymbol = Symbol('watermark-dom');
export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.body)) { export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.body)) {
let func: Fn = () => {};
const id = domSymbol.toString(); const id = domSymbol.toString();
const clear = () => { const clear = () => {
const domId = document.getElementById(id); const domId = document.getElementById(id);
...@@ -10,6 +11,7 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo ...@@ -10,6 +11,7 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo
const el = unref(appendEl); const el = unref(appendEl);
el && el.removeChild(domId); el && el.removeChild(domId);
} }
window.addEventListener('resize', func);
}; };
const createWatermark = (str: string) => { const createWatermark = (str: string) => {
clear(); clear();
...@@ -45,7 +47,7 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo ...@@ -45,7 +47,7 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo
function setWatermark(str: string) { function setWatermark(str: string) {
createWatermark(str); createWatermark(str);
const func = () => { func = () => {
createWatermark(str); createWatermark(str);
}; };
window.addEventListener('resize', func); window.addEventListener('resize', func);
...@@ -53,7 +55,6 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo ...@@ -53,7 +55,6 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo
if (instance) { if (instance) {
onBeforeUnmount(() => { onBeforeUnmount(() => {
clear(); clear();
window.addEventListener('resize', func);
}); });
} }
} }
......
...@@ -18,7 +18,7 @@ export default { ...@@ -18,7 +18,7 @@ export default {
{ {
path: '/icon', path: '/icon',
name: 'IconDemo', name: 'IconDemo',
component: () => import('/@/views/demo/comp/icon/index.vue'), component: () => import('/@/views/demo/feat/icon/index.vue'),
meta: { meta: {
title: '图标', title: '图标',
}, },
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
{ {
path: '/click-out-side', path: '/click-out-side',
name: 'ClickOutSideDemo', name: 'ClickOutSideDemo',
component: () => import('/@/views/demo/comp/click-out-side/index.vue'), component: () => import('/@/views/demo/feat/click-out-side/index.vue'),
meta: { meta: {
title: 'ClickOutSide组件', title: 'ClickOutSide组件',
}, },
......
...@@ -7,8 +7,6 @@ ...@@ -7,8 +7,6 @@
show-icon show-icon
/> />
<Alert message="按钮扩展" type="info" show-icon class="mt-4" />
<div class="my-2"> <div class="my-2">
<h3>success</h3> <h3>success</h3>
<a-button color="success">成功</a-button> <a-button color="success">成功</a-button>
......
<template> <template>
<div class="px-10"> <div class="p-10">
<Alert message="点内外部触发事件" show-icon class="mt-4"></Alert> <Alert message="点内外部触发事件" show-icon></Alert>
<ClickOutSide @clickOutside="handleClickOutside" class="flex justify-center mt-10"> <ClickOutSide @clickOutside="handleClickOutside" class="flex justify-center mt-10">
<div @click="innerClick" class="demo-box"> <div @click="innerClick" class="demo-box">
{{ text }} {{ text }}
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
<CollapseContainer title="Simple"> <CollapseContainer title="Simple">
<a-button type="primary" @contextmenu="handleContext">Right Click on me</a-button> <a-button type="primary" @contextmenu="handleContext">Right Click on me</a-button>
</CollapseContainer> </CollapseContainer>
<CollapseContainer title="Multiple" class="mt-4">
<a-button type="primary" @contextmenu="handleMultipleContext">Right Click on me</a-button>
</CollapseContainer>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
...@@ -36,7 +40,44 @@ ...@@ -36,7 +40,44 @@
], ],
}); });
} }
return { handleContext };
function handleMultipleContext(e: MouseEvent) {
createContextMenu({
event: e,
items: [
{
label: 'New',
icon: 'ant-design:plus-outlined',
children: [
{
label: 'New1-1',
icon: 'ant-design:plus-outlined',
divider: true,
children: [
{
label: 'New1-1-1',
handler: () => {
createMessage.success('click new');
},
},
{
label: 'New1-2-1',
disabled: true,
},
],
},
{
label: 'New1-2',
icon: 'ant-design:plus-outlined',
},
],
},
],
});
}
return { handleContext, handleMultipleContext };
}, },
}); });
</script> </script>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
</div> </div>
</CollapseContainer> </CollapseContainer>
<CollapseContainer title="IconIfy 组件使用" class="mt-5"> <CollapseContainer title="IconIfy 组件使用" class="my-5">
<div class="flex justify-around flex-wrap"> <div class="flex justify-around flex-wrap">
<Icon icon="fa-solid:address-book" :size="30" /> <Icon icon="fa-solid:address-book" :size="30" />
<Icon icon="mdi-light:bank" :size="30" /> <Icon icon="mdi-light:bank" :size="30" />
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
<Alert <Alert
show-icon show-icon
class="mt-5"
message="推荐使用Iconify组件" message="推荐使用Iconify组件"
description="Icon组件基本包含所有的图标,在下面网址内你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。唯一不足的可能就是需要连接外网进行使用。" description="Icon组件基本包含所有的图标,在下面网址内你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。唯一不足的可能就是需要连接外网进行使用。"
/> />
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册