mirror of
https://github.com/vbenjs/vben-admin-thin-next.git
synced 2025-01-24 02:00:22 +08:00
perf: perf context menu
This commit is contained in:
parent
41d79008c5
commit
6e03e05032
@ -1,62 +1,2 @@
|
||||
import contextMenuVue from './src/index';
|
||||
import { isClient } from '/@/utils/is';
|
||||
import { Options, Props } from './src/types';
|
||||
import { createVNode, render } from 'vue';
|
||||
const menuManager: {
|
||||
domList: Element[];
|
||||
resolve: Fn;
|
||||
} = {
|
||||
domList: [],
|
||||
resolve: () => {},
|
||||
};
|
||||
export const createContextMenu = function (options: Options) {
|
||||
const { event } = options || {};
|
||||
try {
|
||||
event.preventDefault();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (!isClient) return;
|
||||
return new Promise((resolve) => {
|
||||
const container = document.createElement('div');
|
||||
const propsData: Partial<Props> = {};
|
||||
if (options.styles !== undefined) propsData.styles = options.styles;
|
||||
if (options.items !== undefined) propsData.items = options.items;
|
||||
if (options.event !== undefined) {
|
||||
propsData.customEvent = event;
|
||||
propsData.axis = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
const vm = createVNode(contextMenuVue, propsData);
|
||||
render(vm, container);
|
||||
const bodyClick = function () {
|
||||
menuManager.resolve('');
|
||||
};
|
||||
menuManager.domList.push(container);
|
||||
const remove = function () {
|
||||
menuManager.domList.forEach((dom: Element) => {
|
||||
try {
|
||||
document.body.removeChild(dom);
|
||||
} catch (error) {}
|
||||
});
|
||||
document.body.removeEventListener('click', bodyClick);
|
||||
document.body.removeEventListener('scroll', bodyClick);
|
||||
};
|
||||
menuManager.resolve = function (...arg: any) {
|
||||
resolve(arg[0]);
|
||||
remove();
|
||||
};
|
||||
remove();
|
||||
document.body.appendChild(container);
|
||||
document.body.addEventListener('click', bodyClick);
|
||||
document.body.addEventListener('scroll', bodyClick);
|
||||
});
|
||||
};
|
||||
export const unMountedContextMenu = function () {
|
||||
if (menuManager) {
|
||||
menuManager.resolve('');
|
||||
menuManager.domList = [];
|
||||
}
|
||||
};
|
||||
|
||||
export { createContextMenu, destroyContextMenu } from './src/createContextMenu';
|
||||
export * from './src/types';
|
||||
|
73
src/components/ContextMenu/src/createContextMenu.ts
Normal file
73
src/components/ContextMenu/src/createContextMenu.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import contextMenuVue from './index';
|
||||
import { isClient } from '/@/utils/is';
|
||||
import { CreateContextOptions, ContextMenuProps } from './types';
|
||||
import { createVNode, render } from 'vue';
|
||||
|
||||
const menuManager: {
|
||||
domList: Element[];
|
||||
resolve: Fn;
|
||||
} = {
|
||||
domList: [],
|
||||
resolve: () => {},
|
||||
};
|
||||
|
||||
export const createContextMenu = function (options: CreateContextOptions) {
|
||||
const { event } = options || {};
|
||||
|
||||
event && event?.preventDefault();
|
||||
|
||||
if (!isClient) return;
|
||||
return new Promise((resolve) => {
|
||||
const body = document.body;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const propsData: Partial<ContextMenuProps> = {};
|
||||
if (options.styles) {
|
||||
propsData.styles = options.styles;
|
||||
}
|
||||
|
||||
if (options.items) {
|
||||
propsData.items = options.items;
|
||||
}
|
||||
|
||||
if (options.event) {
|
||||
propsData.customEvent = event;
|
||||
propsData.axis = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
|
||||
const vm = createVNode(contextMenuVue, propsData);
|
||||
render(vm, container);
|
||||
|
||||
const handleClick = function () {
|
||||
menuManager.resolve('');
|
||||
};
|
||||
|
||||
menuManager.domList.push(container);
|
||||
|
||||
const remove = function () {
|
||||
menuManager.domList.forEach((dom: Element) => {
|
||||
try {
|
||||
dom && body.removeChild(dom);
|
||||
} catch (error) {}
|
||||
});
|
||||
body.removeEventListener('click', handleClick);
|
||||
body.removeEventListener('scroll', handleClick);
|
||||
};
|
||||
|
||||
menuManager.resolve = function (...arg: any) {
|
||||
remove();
|
||||
resolve(arg[0]);
|
||||
};
|
||||
remove();
|
||||
body.appendChild(container);
|
||||
body.addEventListener('click', handleClick);
|
||||
body.addEventListener('scroll', handleClick);
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyContextMenu = function () {
|
||||
if (menuManager) {
|
||||
menuManager.resolve('');
|
||||
menuManager.domList = [];
|
||||
}
|
||||
};
|
@ -1,22 +1,28 @@
|
||||
@import (reference) '../../../design/index.less';
|
||||
|
||||
@default-height: 42px !important;
|
||||
|
||||
@small-height: 36px !important;
|
||||
|
||||
@large-height: 36px !important;
|
||||
|
||||
.item-style() {
|
||||
li {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 46px !important;
|
||||
height: @default-height;
|
||||
margin: 0 !important;
|
||||
line-height: 46px;
|
||||
line-height: @default-height;
|
||||
|
||||
span {
|
||||
line-height: 46px;
|
||||
line-height: @default-height;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(.ant-menu-item-disabled):hover {
|
||||
color: @text-color-base;
|
||||
background: #eee;
|
||||
}
|
||||
@ -27,10 +33,9 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1500;
|
||||
z-index: 200;
|
||||
display: block;
|
||||
width: 156px;
|
||||
min-width: 10rem;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background-color: #fff;
|
||||
|
@ -1,99 +1,98 @@
|
||||
import {
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
reactive,
|
||||
computed,
|
||||
ref,
|
||||
unref,
|
||||
onUnmounted,
|
||||
} from 'vue';
|
||||
import './index.less';
|
||||
|
||||
import type { ContextMenuItem, ItemContentProps } from './types';
|
||||
import type { FunctionalComponent, CSSProperties } from 'vue';
|
||||
|
||||
import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue';
|
||||
|
||||
import { props } from './props';
|
||||
import Icon from '/@/components/Icon';
|
||||
import { Menu, Divider } from 'ant-design-vue';
|
||||
|
||||
import type { ContextMenuItem } from './types';
|
||||
import { props } from './props';
|
||||
|
||||
import './index.less';
|
||||
const prefixCls = 'context-menu';
|
||||
|
||||
const ItemContent: FunctionalComponent<ItemContentProps> = (props) => {
|
||||
const { item } = props;
|
||||
return (
|
||||
<span style="display: inline-block; width: 100%;" onClick={props.handler.bind(null, item)}>
|
||||
{props.showIcon && item.icon && <Icon class="mr-2" icon={item.icon} />}
|
||||
<span>{item.label}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ContextMenu',
|
||||
props,
|
||||
setup(props) {
|
||||
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
|
||||
const state = reactive({
|
||||
show: false,
|
||||
});
|
||||
const wrapRef = ref<ElRef>(null);
|
||||
const showRef = ref(false);
|
||||
|
||||
const getStyle = computed(
|
||||
(): CSSProperties => {
|
||||
const { axis, items, styles, width } = props;
|
||||
const { x, y } = axis || { x: 0, y: 0 };
|
||||
const menuHeight = (items || []).length * 40;
|
||||
const menuWidth = width;
|
||||
const body = document.body;
|
||||
|
||||
const left = body.clientWidth < x + menuWidth ? x - menuWidth : x;
|
||||
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
|
||||
return {
|
||||
...styles,
|
||||
width: `${width}px`,
|
||||
left: `${left + 1}px`,
|
||||
top: `${top + 1}px`,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
state.show = true;
|
||||
});
|
||||
nextTick(() => (showRef.value = true));
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
const el = unref(wrapRef);
|
||||
el && document.body.removeChild(el);
|
||||
});
|
||||
const getStyle = computed(() => {
|
||||
const { axis, items, styles, width } = props;
|
||||
const { x, y } = axis || { x: 0, y: 0 };
|
||||
const menuHeight = (items || []).length * 40;
|
||||
const menuWidth = width;
|
||||
const body = document.body;
|
||||
return {
|
||||
...(styles as any),
|
||||
width: `${width}px`,
|
||||
left: (body.clientWidth < x + menuWidth ? x - menuWidth : x) + 'px',
|
||||
top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
|
||||
};
|
||||
});
|
||||
|
||||
function handleAction(item: ContextMenuItem, e: MouseEvent) {
|
||||
state.show = false;
|
||||
const { handler, disabled } = item;
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (disabled) return;
|
||||
showRef.value = false;
|
||||
|
||||
handler && handler();
|
||||
}
|
||||
|
||||
function renderContent(item: ContextMenuItem) {
|
||||
const { icon, label } = item;
|
||||
|
||||
const { showIcon } = props;
|
||||
return (
|
||||
<span style="display: inline-block; width: 100%;" onClick={handleAction.bind(null, item)}>
|
||||
{showIcon && icon && <Icon class="mr-2" icon={icon} />}
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
handler?.();
|
||||
}
|
||||
|
||||
function renderMenuItem(items: ContextMenuItem[]) {
|
||||
return items.map((item, index) => {
|
||||
return items.map((item) => {
|
||||
const { disabled, label, children, divider = false } = item;
|
||||
|
||||
const DividerComp = divider ? <Divider key={`d-${index}`} /> : null;
|
||||
const DividerComp = divider ? <Divider key={`d-${label}`} /> : null;
|
||||
if (!children || children.length === 0) {
|
||||
return [
|
||||
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
|
||||
{() => [renderContent(item)]}
|
||||
</Menu.Item>,
|
||||
DividerComp,
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
|
||||
{() => [
|
||||
<ItemContent showIcon={props.showIcon} item={item} handler={handleAction} />,
|
||||
]}
|
||||
</Menu.Item>
|
||||
{DividerComp}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return !state.show ? null : (
|
||||
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup `}>
|
||||
if (!unref(showRef)) return null;
|
||||
|
||||
return (
|
||||
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}>
|
||||
{{
|
||||
title: () => renderContent(item),
|
||||
default: () => [renderMenuItem(children)],
|
||||
title: () => (
|
||||
<ItemContent showIcon={props.showIcon} item={item} handler={handleAction} />
|
||||
),
|
||||
default: () => renderMenuItem(children),
|
||||
}}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
@ -101,11 +100,12 @@ export default defineComponent({
|
||||
}
|
||||
return () => {
|
||||
const { items } = props;
|
||||
return !state.show ? null : (
|
||||
if (!unref(showRef)) return null;
|
||||
return (
|
||||
<Menu
|
||||
inlineIndent={12}
|
||||
mode="vertical"
|
||||
class={[prefixCls]}
|
||||
class={prefixCls}
|
||||
ref={wrapRef}
|
||||
style={unref(getStyle)}
|
||||
>
|
||||
|
@ -1,16 +1,16 @@
|
||||
import type { PropType } from 'vue';
|
||||
import type { PropType, CSSProperties } from 'vue';
|
||||
import type { Axis, ContextMenuItem } from './types';
|
||||
export const props = {
|
||||
width: {
|
||||
type: Number as PropType<number>,
|
||||
default: 180,
|
||||
default: 156,
|
||||
},
|
||||
customEvent: {
|
||||
type: Object as PropType<Event>,
|
||||
default: null,
|
||||
},
|
||||
styles: {
|
||||
type: Object as PropType<any>,
|
||||
type: Object as PropType<CSSProperties>,
|
||||
default: null,
|
||||
},
|
||||
showIcon: {
|
||||
@ -31,8 +31,4 @@ export const props = {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
type: Function as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
|
@ -11,15 +11,14 @@ export interface ContextMenuItem {
|
||||
divider?: boolean;
|
||||
children?: ContextMenuItem[];
|
||||
}
|
||||
export interface Options {
|
||||
export interface CreateContextOptions {
|
||||
event: MouseEvent;
|
||||
icon?: string;
|
||||
styles?: any;
|
||||
items?: ContextMenuItem[];
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
resolve?: (...arg: any) => void;
|
||||
export interface ContextMenuProps {
|
||||
event?: MouseEvent;
|
||||
styles?: any;
|
||||
items: ContextMenuItem[];
|
||||
@ -27,4 +26,10 @@ export type Props = {
|
||||
axis?: Axis;
|
||||
width?: number;
|
||||
showIcon?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ItemContentProps {
|
||||
showIcon: boolean;
|
||||
item: ContextMenuItem;
|
||||
handler: Fn;
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export default defineComponent({
|
||||
props: basicProps,
|
||||
emits: ['visible-change', 'ok', 'close', 'register'],
|
||||
setup(props, { slots, emit, attrs }) {
|
||||
const scrollRef = ref<any>(null);
|
||||
const scrollRef = ref<ElRef>(null);
|
||||
|
||||
const visibleRef = ref(false);
|
||||
const propsRef = ref<Partial<DrawerProps> | null>(null);
|
||||
|
@ -22,7 +22,7 @@ export default defineComponent({
|
||||
setup(props, { slots, emit, attrs }) {
|
||||
const visibleRef = ref(false);
|
||||
const propsRef = ref<Partial<ModalProps> | null>(null);
|
||||
const modalWrapperRef = ref<any>(null);
|
||||
const modalWrapperRef = ref<ComponentRef>(null);
|
||||
// modal Bottom and top height
|
||||
const extHeightRef = ref(0);
|
||||
// Unexpanded height of the popup
|
||||
|
@ -55,7 +55,7 @@ export default defineComponent({
|
||||
emits: ['heightChange', 'getExtHeight'],
|
||||
setup(props: ModalWrapperProps, { slots, emit }) {
|
||||
const wrapperRef = ref<HTMLElement | null>(null);
|
||||
const spinRef = ref<any>(null);
|
||||
const spinRef = ref<ComponentRef>(null);
|
||||
const realHeightRef = ref(0);
|
||||
// 重试次数
|
||||
// let tryCount = 0;
|
||||
@ -126,6 +126,8 @@ export default defineComponent({
|
||||
await nextTick();
|
||||
const spinEl = unref(spinRef);
|
||||
|
||||
if (!spinEl) return;
|
||||
|
||||
const spinContainerEl = spinEl.$el.querySelector('.ant-spin-container') as HTMLElement;
|
||||
if (!spinContainerEl) return;
|
||||
|
||||
|
@ -74,7 +74,7 @@
|
||||
components: { Table, BasicForm },
|
||||
emits: ['fetch-success', 'fetch-error', 'selection-change', 'register'],
|
||||
setup(props, { attrs, emit, slots }) {
|
||||
const tableElRef = ref<any>(null);
|
||||
const tableElRef = ref<ComponentRef>(null);
|
||||
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
|
||||
const innerPropsRef = ref<Partial<BasicTableProps>>();
|
||||
const [registerForm, { getFieldsValue }] = useForm();
|
||||
@ -241,10 +241,8 @@
|
||||
if (unref(getMergeProps).showSummary) {
|
||||
nextTick(() => {
|
||||
const tableEl = unref(tableElRef);
|
||||
if (!tableEl) {
|
||||
return;
|
||||
}
|
||||
const bodyDomList = tableEl.$el.querySelectorAll('.ant-table-body') as HTMLDivElement[];
|
||||
if (!tableEl) return;
|
||||
const bodyDomList = tableEl.$el.querySelectorAll('.ant-table-body');
|
||||
const bodyDom = bodyDomList[0];
|
||||
useEventListener({
|
||||
el: bodyDom,
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { onUnmounted, getCurrentInstance } from 'vue';
|
||||
import { createContextMenu, unMountedContextMenu } from '/@/components/ContextMenu';
|
||||
import { createContextMenu, destroyContextMenu } from '/@/components/ContextMenu';
|
||||
import type { ContextMenuItem } from '/@/components/ContextMenu';
|
||||
export type { ContextMenuItem };
|
||||
export function useContextMenu(authRemove = true) {
|
||||
if (getCurrentInstance() && authRemove) {
|
||||
onUnmounted(() => {
|
||||
unMountedContextMenu();
|
||||
destroyContextMenu();
|
||||
});
|
||||
}
|
||||
return [createContextMenu, unMountedContextMenu];
|
||||
return [createContextMenu, destroyContextMenu];
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
> .basic-loading {
|
||||
margin-bottom: 30%;
|
||||
margin-bottom: 15%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,10 +60,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
let logoEl: Element | null;
|
||||
let logoEl: Element | null | undefined;
|
||||
|
||||
const logoWidthRef = ref(200);
|
||||
const logoRef = ref<any>(null);
|
||||
const logoRef = ref<ComponentRef>(null);
|
||||
const { refreshPage } = useTabs();
|
||||
|
||||
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getTopMenuAlign } = useMenuSetting();
|
||||
@ -91,7 +91,7 @@ export default defineComponent({
|
||||
if (!unref(getShowTopMenu)) return;
|
||||
let width = 0;
|
||||
if (!logoEl) {
|
||||
logoEl = logoRef.value.$el;
|
||||
logoEl = unref(logoRef)?.$el;
|
||||
} else {
|
||||
width += logoEl.clientWidth;
|
||||
}
|
||||
|
@ -39,26 +39,30 @@ export default defineComponent({
|
||||
return unref(getShowMultipleTab) && !unref(getFullContent);
|
||||
});
|
||||
|
||||
const getPlaceholderDomStyle = computed(() => {
|
||||
return {
|
||||
height: `${unref(placeholderHeightRef)}px`,
|
||||
};
|
||||
});
|
||||
const getPlaceholderDomStyle = computed(
|
||||
(): CSSProperties => {
|
||||
return {
|
||||
height: `${unref(placeholderHeightRef)}px`,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const getIsShowPlaceholderDom = computed(() => {
|
||||
return unref(getFixed) || unref(getShowFullHeaderRef);
|
||||
});
|
||||
|
||||
const getWrapStyle = computed(() => {
|
||||
const style: CSSProperties = {};
|
||||
if (unref(getFixed)) {
|
||||
style.width = unref(getCalcContentWidth);
|
||||
const getWrapStyle = computed(
|
||||
(): CSSProperties => {
|
||||
const style: CSSProperties = {};
|
||||
if (unref(getFixed)) {
|
||||
style.width = unref(getCalcContentWidth);
|
||||
}
|
||||
if (unref(getShowFullHeaderRef)) {
|
||||
style.top = `${unref(fullHeaderHeightRef)}px`;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
if (unref(getShowFullHeaderRef)) {
|
||||
style.top = `${unref(fullHeaderHeightRef)}px`;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
);
|
||||
|
||||
const getIsFixed = computed(() => {
|
||||
return unref(getFixed) || unref(getShowFullHeaderRef);
|
||||
|
@ -20,7 +20,6 @@
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
// flex-grow: 1;
|
||||
align-items: center;
|
||||
|
||||
.layout-trigger {
|
||||
|
@ -40,9 +40,12 @@
|
||||
height: 12px;
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
visibility: hidden;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
|
||||
svg {
|
||||
width: 0.8em;
|
||||
}
|
||||
@ -69,6 +72,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-tabs-close-x {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.7em;
|
||||
fill: @white;
|
||||
|
Loading…
Reference in New Issue
Block a user