feat: 表格批量操作根据宽度自动收缩

This commit is contained in:
RubyLiu 2023-11-08 19:13:42 +08:00 committed by rubylliu
parent c322f69f2c
commit 3d0d242515
5 changed files with 193 additions and 32 deletions

View File

@ -58,6 +58,7 @@
"pinia": "^2.1.6", "pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0", "pinia-plugin-persistedstate": "^3.2.0",
"query-string": "^8.1.0", "query-string": "^8.1.0",
"resize-observer-polyfill": "^1.5.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-echarts": "^6.6.1", "vue-echarts": "^6.6.1",

View File

@ -164,16 +164,20 @@
</a-table> </a-table>
<div <div
v-if="showBatchAction || attrs.showPagination" v-if="showBatchAction || attrs.showPagination"
class="mt-[16px] flex h-[32px] w-[100%] flex-row flex-nowrap items-center justify-end px-0" class="mt-[16px] flex h-[32px] min-w-[1000px] flex-row flex-nowrap items-center"
:class="{ 'justify-between': showBatchAction, 'min-w-[934px]': attrs.selectable }" :class="{ 'justify-between': showBatchAction }"
> >
<div class="flex flex-grow">
<batch-action <batch-action
v-if="showBatchAction" v-if="showBatchAction"
class="flex-1"
:select-row-count="selectCurrent" :select-row-count="selectCurrent"
:action-config="props.actionConfig" :action-config="props.actionConfig"
@batch-action="handleBatchAction" @batch-action="handleBatchAction"
@clear="emit('clearSelector')" @clear="emit('clearSelector')"
/> />
</div>
<div class="min-w-[500px]">
<ms-pagination <ms-pagination
v-if="attrs.showPagination" v-if="attrs.showPagination"
v-show="props.selectorStatus !== SelectAllEnum.CURRENT" v-show="props.selectorStatus !== SelectAllEnum.CURRENT"
@ -184,6 +188,7 @@
@page-size-change="pageSizeChange" @page-size-change="pageSizeChange"
/> />
</div> </div>
</div>
<ColumnSelector <ColumnSelector
v-model:visible="columnSelectorVisible" v-model:visible="columnSelectorVisible"
:show-jump-method="(attrs.showJumpMethod as boolean)" :show-jump-method="(attrs.showJumpMethod as boolean)"

View File

@ -1,8 +1,8 @@
<template> <template>
<div v-if="props.actionConfig" class="ms-table__patch-action"> <div v-if="props.actionConfig" ref="refWrapper" class="flex flex-row flex-nowrap">
<span class="title">{{ t('msTable.batch.selected', { count: props.selectRowCount }) }}</span> <div class="title">{{ t('msTable.batch.selected', { count: props.selectRowCount }) }}</div>
<template v-for="(element, idx) in props.actionConfig.baseAction" :key="element.label"> <template v-for="(element, idx) in baseAction" :key="element.label">
<a-divider v-if="element.isDivider" class="mx-0 my-[6px]" /> <a-divider v-if="element.isDivider" class="divider mx-0 my-[6px]" />
<a-button <a-button
v-else v-else
class="ml-[12px]" class="ml-[12px]"
@ -15,11 +15,11 @@
>{{ t(element.label as string) }}</a-button >{{ t(element.label as string) }}</a-button
> >
</template> </template>
<div v-if="props.actionConfig.moreAction" class="relative top-[2px] ml-[16px] inline-block"> <div v-if="props.actionConfig.moreAction" class="drop-down relative ml-[16px] inline-block">
<a-dropdown position="tr" @select="handleSelect"> <a-dropdown position="tr" @select="handleSelect">
<a-button type="outline"><MsIcon type="icon-icon_more_outlined" /></a-button> <a-button type="outline"><MsIcon type="icon-icon_more_outlined" /></a-button>
<template #content> <template #content>
<template v-for="element in props.actionConfig.moreAction" :key="element.label"> <template v-for="element in moreAction" :key="element.label">
<a-divider v-if="element.isDivider" margin="4px" /> <a-divider v-if="element.isDivider" margin="4px" />
<a-doption v-else :value="element" :class="{ delete: element.danger }"> <a-doption v-else :value="element" :class="{ delete: element.danger }">
{{ t(element.label as string) }} {{ t(element.label as string) }}
@ -28,7 +28,7 @@
</template> </template>
</a-dropdown> </a-dropdown>
</div> </div>
<a-button class="ml-[16px]" type="text" @click="emit('clear')">{{ t('msTable.batch.clear') }}</a-button> <a-button class="clear-btn ml-[16px]" type="text" @click="emit('clear')">{{ t('msTable.batch.clear') }}</a-button>
</div> </div>
</template> </template>
@ -36,10 +36,15 @@
import MsIcon from '../ms-icon-font/index.vue'; import MsIcon from '../ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { getNodeWidth } from '@/utils/dom';
import { BatchActionConfig, BatchActionParams } from './type'; import { BatchActionConfig, BatchActionParams } from './type';
import ResizeObserver from 'resize-observer-polyfill';
const { t } = useI18n(); const { t } = useI18n();
const refWrapper = ref<HTMLElement>();
const props = defineProps<{ const props = defineProps<{
selectRowCount?: number; selectRowCount?: number;
actionConfig?: BatchActionConfig; actionConfig?: BatchActionConfig;
@ -48,17 +53,114 @@
(e: 'batchAction', value: BatchActionParams): void; (e: 'batchAction', value: BatchActionParams): void;
(e: 'clear'): void; (e: 'clear'): void;
}>(); }>();
const baseAction = ref<BatchActionConfig['baseAction']>([]);
const moreAction = ref<BatchActionConfig['moreAction']>([]);
// action
const allAction = ref<BatchActionConfig['baseAction']>([]);
const lastVisibleIndex = ref<number | null>(null);
const refResizeObserver = ref<ResizeObserver>();
const titleClass = 'title';
const dividerClass = 'divider';
const dropDownClass = 'drop-down';
const clearBtnClass = 'clear-btn';
const handleSelect = (item: string | number | Record<string, any> | undefined | BatchActionParams) => { const handleSelect = (item: string | number | Record<string, any> | undefined | BatchActionParams) => {
emit('batchAction', item as BatchActionParams); emit('batchAction', item as BatchActionParams);
}; };
const computedLastVisibleIndex = () => {
if (!refWrapper.value) {
return;
}
const wrapperWidth = getNodeWidth(refWrapper.value);
const childNodeList = [].slice.call(refWrapper.value.children) as HTMLElement[];
let totalWidth = 0;
let menuItemIndex = 0;
for (let i = 0; i < childNodeList.length; i++) {
const node = childNodeList[i];
const classNames = node.className.split(' ');
const isTitle = classNames.includes(titleClass);
const isDivider = classNames.includes(dividerClass);
const isDropDown = classNames.includes(dropDownClass);
const isClearBtn = classNames.includes(clearBtnClass);
if (isDivider) {
totalWidth += 16;
} else if (isTitle) {
// title100px
totalWidth += 100;
} else if (isDropDown) {
// dropDown48px + MarginLeft 16px
totalWidth += 64;
} else if (isClearBtn) {
// 60px + MarginLeft 16px
totalWidth += 76;
} else {
// + marginLeft 16px
totalWidth += getNodeWidth(node) + 16;
menuItemIndex++;
}
if (totalWidth > wrapperWidth) {
lastVisibleIndex.value = menuItemIndex - 1;
return;
}
}
lastVisibleIndex.value = null;
setTimeout(() => {
baseAction.value = props.actionConfig?.baseAction || [];
moreAction.value = props.actionConfig?.moreAction || [];
}, 0);
};
watch(
() => props.actionConfig,
(value) => {
if (value) {
allAction.value = [...value.baseAction];
baseAction.value = [...value.baseAction];
if (value.moreAction) {
allAction.value = [...allAction.value, ...value.moreAction];
moreAction.value = [...value.moreAction];
}
}
},
{ immediate: true }
);
watch(
() => lastVisibleIndex.value,
(value) => {
if (value !== null) {
baseAction.value = allAction.value.slice(0, value);
moreAction.value = allAction.value.slice(value);
}
}
);
onMounted(() => {
refResizeObserver.value = new ResizeObserver((entries: ResizeObserverEntry[]) => {
entries.forEach(computedLastVisibleIndex);
});
if (refWrapper.value) {
refResizeObserver.value.observe(refWrapper.value);
}
});
onUnmounted(() => {
refResizeObserver.value?.disconnect();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.ms-table__patch-action {
.title { .title {
display: flex;
align-items: center;
width: 100px;
color: var(--color-text-2); color: var(--color-text-2);
} }
}
.delete { .delete {
border-color: rgb(var(--danger-6)) !important; border-color: rgb(var(--danger-6)) !important;
color: rgb(var(--danger-6)); color: rgb(var(--danger-6));

View File

@ -3,4 +3,13 @@ export function addPixelValues(...values: string[]) {
const totalValue = pixelValues.reduce((acc, val) => acc + val, 0); const totalValue = pixelValues.reduce((acc, val) => acc + val, 0);
return `${totalValue}px`; return `${totalValue}px`;
} }
/**
* px number
* @param str size
* @returns number
*/
export function translatePxToNumber(str: string): number {
const result = Number(str.replace('px', ''));
return Number.isNaN(result) ? 0 : result;
}
export default {}; export default {};

View File

@ -1,3 +1,6 @@
/**
*
*/
export interface ScrollToViewOptions { export interface ScrollToViewOptions {
behavior?: 'auto' | 'smooth'; behavior?: 'auto' | 'smooth';
block?: 'start' | 'center' | 'end' | 'nearest'; block?: 'start' | 'center' | 'end' | 'nearest';
@ -19,11 +22,16 @@ export function scrollIntoView(targetRef: HTMLElement | Element | null, options:
targetRef?.scrollIntoView(scrollOptions); targetRef?.scrollIntoView(scrollOptions);
} }
/**
*
*/
export const NOOP = () => { export const NOOP = () => {
return undefined; return undefined;
}; };
// 判断是否为服务端渲染 /**
*
*/
export const isServerRendering = (() => { export const isServerRendering = (() => {
try { try {
return !(typeof window !== 'undefined' && document !== undefined); return !(typeof window !== 'undefined' && document !== undefined);
@ -32,7 +40,9 @@ export const isServerRendering = (() => {
} }
})(); })();
// 监听事件 /**
*
*/
export const on = (() => { export const on = (() => {
if (isServerRendering) { if (isServerRendering) {
return NOOP; return NOOP;
@ -47,7 +57,9 @@ export const on = (() => {
}; };
})(); })();
// 移除监听事件 /**
*
*/
export const off = (() => { export const off = (() => {
if (isServerRendering) { if (isServerRendering) {
return NOOP; return NOOP;
@ -61,3 +73,35 @@ export const off = (() => {
element.removeEventListener(type, handler as EventListenerOrEventListenerObject, options); element.removeEventListener(type, handler as EventListenerOrEventListenerObject, options);
}; };
})(); })();
/**
*
* @param el
* @returns number
*/
export function getNodeWidth(el: HTMLElement) {
return el && +el.getBoundingClientRect().width.toFixed(2);
}
/**
*
* @param element
* @param prop
* @returns string
*/
export function getStyle(element: HTMLElement | null, prop: string | null) {
if (!element || !prop) return null;
let styleName = prop as keyof CSSStyleDeclaration;
if (styleName === 'float') {
styleName = 'cssFloat';
}
try {
if (document.defaultView) {
const computed = document.defaultView.getComputedStyle(element, '');
return element.style[styleName] || computed ? computed[styleName] : '';
}
} catch (e) {
return element.style[styleName];
}
return null;
}