Skip to content

Commit

Permalink
新增支持虚拟滚动的表格组件 (#1796)
Browse files Browse the repository at this point in the history
  • Loading branch information
xingyan95 authored Mar 26, 2024
1 parent e89da31 commit 2907c9c
Show file tree
Hide file tree
Showing 30 changed files with 5,131 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/devui-vue/devui-cli/templates/vue-devui.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { App } from 'vue';
${imports.join('\n')}
import './style/devui.scss';
import './style/index.scss';
const installs = [
${installs.join(',\n ')}
Expand Down
14 changes: 14 additions & 0 deletions packages/devui-vue/devui/data-grid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { App } from "vue";
import DataGrid from './src/data-grid';

export * from './src/data-grid-types';
export { DataGrid }

export default {
title: 'DataGrid 数据表格',
category: '数据展示',
status: '100%',
install(app: App): void {
app.component(DataGrid.name, DataGrid);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { defineComponent, inject, ref, watch, onMounted, onBeforeMount } from 'vue';
import { useNamespace } from '../../../shared/hooks/use-namespace';
import GridHead from './grid-head';
import GridBody from './grid-body';
import { DataGridInjectionKey } from '../data-grid-types';
import type { DataGridContext } from '../data-grid-types';
import { useDataGridLazy } from '../composables/use-data-grid-scroll';

export default defineComponent({
name: 'FixHeadGrid',
setup() {
const ns = useNamespace('data-grid');
const {
scrollRef,
headBoxRef,
showHeader,
bodyContentWidth,
bodyContentHeight,
renderColumnData,
renderFixedLeftColumnData,
renderFixedRightColumnData,
renderRowData,
translateX,
translateY,
bodyScrollLeft,
rootCtx,
} = inject(DataGridInjectionKey) as DataGridContext;
const hasScrollbar = ref(false);
let resizeObserver: ResizeObserver;
useDataGridLazy(scrollRef);

const isHaveScrollbar = () => {
if (scrollRef.value) {
hasScrollbar.value = scrollRef.value.scrollHeight > scrollRef.value.clientHeight;
}
};

watch(bodyContentHeight, isHaveScrollbar, { immediate: true });

onMounted(() => {
if (scrollRef.value) {
resizeObserver = new ResizeObserver(isHaveScrollbar);
resizeObserver.observe(scrollRef.value);
}
});

onBeforeMount(() => {
resizeObserver?.disconnect();
});

return () => (
<div>
{showHeader.value && (
<div ref={headBoxRef} class={ns.e('head-wrapper')} style={{ 'overflow-y': hasScrollbar.value ? 'scroll' : 'auto' }}>
<div class={ns.e('x-space')} style={{ width: bodyContentWidth.value + 'px' }}></div>
<GridHead
columnData={renderColumnData.value}
leftColumnData={renderFixedLeftColumnData.value}
rightColumnData={renderFixedRightColumnData.value}
translateX={translateX.value}
bodyScrollLeft={bodyScrollLeft.value}
/>
</div>
)}
<div ref={scrollRef} class={[ns.e('body-wrapper'), 'devui-scroll-overlay']}>
<div class={ns.e('x-space')} style={{ width: bodyContentWidth.value + 'px' }}></div>
<div class={ns.e('y-space')} style={{ height: bodyContentHeight.value + 'px' }}></div>

{Boolean(renderRowData.value.length) ? (
<GridBody
rowData={renderRowData.value}
columnData={renderColumnData.value}
leftColumnData={renderFixedLeftColumnData.value}
rightColumnData={renderFixedRightColumnData.value}
translateX={translateX.value}
translateY={translateY.value}
bodyScrollLeft={bodyScrollLeft.value}
/>
) : (
<div class={ns.e('empty')} style={{ left: bodyScrollLeft.value + 'px' }}>
{rootCtx.slots.empty?.()}
</div>
)}
</div>
</div>
);
},
});
135 changes: 135 additions & 0 deletions packages/devui-vue/devui/data-grid/src/components/grid-body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { defineComponent, toRefs, inject, ref, Teleport } from 'vue';
import { FlexibleOverlay } from '../../../overlay';
import GridTd from './grid-td';
import { gridBodyProps, DataGridInjectionKey } from '../data-grid-types';
import type { GridBodyProps, DataGridContext, InnerRowData } from '../data-grid-types';
import { useNamespace } from '../../../shared/hooks/use-namespace';
import { useOverflowTooltip } from '../composables/use-overflow-tooltip';
import { ToggleTreeIcon, DataGridCheckboxClass } from '../const';

export default defineComponent({
name: 'GridBody',
props: gridBodyProps,
setup(props: GridBodyProps) {
const ns = useNamespace('data-grid');
const { rowClass, rootCtx } = inject(DataGridInjectionKey) as DataGridContext;
const { rowData, columnData, leftColumnData, rightColumnData, translateX, translateY, bodyScrollLeft } = toRefs(props);
const currentRowIndex = ref<number | undefined>();
const {
showTooltip,
originRef,
tooltipContent,
tooltipPosition,
tooltipClassName,
onCellMouseenter,
onCellMouseleave,
onOverlayMouseenter,
onOverlayMouseleave
} = useOverflowTooltip();
const trClasses = (rowData: InnerRowData, rowIndex: number) => {
const realRowClass = typeof rowClass.value === 'string' ? rowClass.value : rowClass.value(rowData, rowIndex);
return {
[ns.e('tr')]: true,
[realRowClass]: true,
'hover-tr': currentRowIndex.value === rowIndex,
};
};
const onRowClick = (e: Event, rowData: InnerRowData, rowIndex: number) => {
const composedPath = e.composedPath() as HTMLElement[];
if (composedPath.some((item) => item.classList?.contains(ToggleTreeIcon) || item.classList?.contains(DataGridCheckboxClass))) {
return;
}
rootCtx.emit('rowClick', { row: { ...rowData }, renderRowIndex: rowIndex, flattenRowIndex: rowData.$rowIndex });
};
const onTrMouseenterOrLeave = (rowIndex: number | undefined) => {
currentRowIndex.value = rowIndex;
};

return () => (
<>
{Boolean(leftColumnData.value.length) && (
<div
class={ns.e('sticky-left-body')}
style={{ left: bodyScrollLeft.value + 'px', transform: `translateY(${translateY.value}px)` }}>
{rowData.value.map((itemRow, rowIndex) => (
<div
class={trClasses(itemRow, rowIndex)}
onClick={(e) => onRowClick(e, itemRow, rowIndex)}
onMouseenter={() => onTrMouseenterOrLeave(rowIndex)}
onMouseleave={() => onTrMouseenterOrLeave(undefined)}>
{leftColumnData.value.map((cellData, cellIndex) => (
<GridTd
class={{ [ns.e('last-sticky-left-cell')]: cellIndex === leftColumnData.value.length - 1 }}
rowData={itemRow}
cellData={cellData}
rowIndex={rowIndex}
mouseenterCb={onCellMouseenter}
mouseleaveCb={onCellMouseleave}
/>
))}
</div>
))}
</div>
)}

{Boolean(rightColumnData.value.length) && (
<div
class={ns.e('sticky-right-body')}
style={{ right: `-${bodyScrollLeft.value}px`, transform: `translateY(${translateY.value}px)` }}>
{rowData.value.map((itemRow, rowIndex) => (
<div
class={trClasses(itemRow, rowIndex)}
onClick={(e) => onRowClick(e, itemRow, rowIndex)}
onMouseenter={() => onTrMouseenterOrLeave(rowIndex)}
onMouseleave={() => onTrMouseenterOrLeave(undefined)}>
{rightColumnData.value.map((cellData, cellIndex) => (
<GridTd
class={{ [ns.e('first-sticky-right-cell')]: cellIndex === 0 }}
rowData={itemRow}
cellData={cellData}
rowIndex={rowIndex}
mouseenterCb={onCellMouseenter}
mouseleaveCb={onCellMouseleave}
/>
))}
</div>
))}
</div>
)}

<div class={ns.e('body')} style={{ transform: `translate(${translateX.value}px, ${translateY.value}px)` }}>
{rowData.value.map((itemRow, rowIndex) => (
<div
class={trClasses(itemRow, rowIndex)}
onClick={(e) => onRowClick(e, itemRow, rowIndex)}
onMouseenter={() => onTrMouseenterOrLeave(rowIndex)}
onMouseleave={() => onTrMouseenterOrLeave(undefined)}>
{columnData.value.map((cellData) => (
<GridTd
rowData={itemRow}
cellData={cellData}
rowIndex={rowIndex}
mouseenterCb={onCellMouseenter}
mouseleaveCb={onCellMouseleave}
/>
))}
</div>
))}
</div>
<Teleport to='body'>
<FlexibleOverlay
v-model={showTooltip.value}
origin={originRef.value}
class={[ns.e('tooltip'), tooltipClassName.value]}
position={tooltipPosition.value}
offset={6}
show-arrow
onMouseenter={onOverlayMouseenter}
onMouseleave={onOverlayMouseleave}>
<span>{tooltipContent.value}</span>
</FlexibleOverlay>
</Teleport>
</>
);
},
});
77 changes: 77 additions & 0 deletions packages/devui-vue/devui/data-grid/src/components/grid-head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { defineComponent, toRefs, Teleport } from 'vue';
import { useNamespace } from '../../../shared/hooks/use-namespace';
import { FlexibleOverlay } from '../../../overlay';
import GridTh from './grid-th';
import { gridHeadProps } from '../data-grid-types';
import type { GridHeadProps } from '../data-grid-types';
import { useOverflowTooltip } from '../composables/use-overflow-tooltip';

export default defineComponent({
name: 'GridHead',
props: gridHeadProps,
setup(props: GridHeadProps) {
const ns = useNamespace('data-grid');
const { columnData, leftColumnData, rightColumnData, translateX, bodyScrollLeft } = toRefs(props);
const {
showTooltip,
originRef,
tooltipContent,
tooltipPosition,
tooltipClassName,
onCellMouseenter,
onCellMouseleave,
onOverlayMouseenter,
onOverlayMouseleave
} = useOverflowTooltip();

return () => (
<>
{Boolean(leftColumnData.value.length) && (
<div class={[ns.e('head'), ns.e('sticky-left-head')]} style={{ left: bodyScrollLeft.value + 'px' }}>
{leftColumnData.value.map((item, index) => (
<GridTh
columnConfig={item}
class={{ [ns.e('last-sticky-left-cell')]: index === leftColumnData.value.length - 1 }}
mouseenterCb={onCellMouseenter}
mouseleaveCb={onCellMouseleave}
/>
))}
</div>
)}

{Boolean(rightColumnData.value.length) && (
<div class={[ns.e('head'), ns.e('sticky-right-head')]} style={{ right: `-${bodyScrollLeft.value}px` }}>
{rightColumnData.value.map((item, index) => (
<GridTh
columnConfig={item}
class={{ [ns.e('first-sticky-right-cell')]: index === 0 }}
mouseenterCb={onCellMouseenter}
mouseleaveCb={onCellMouseleave}
/>
))}
</div>
)}

<div class={ns.e('head')} style={{ transform: `translate(${translateX.value}px,0)` }}>
{columnData.value.map((item) => (
<GridTh columnConfig={item} mouseenterCb={onCellMouseenter} mouseleaveCb={onCellMouseleave} />
))}
</div>

<Teleport to='body'>
<FlexibleOverlay
v-model={showTooltip.value}
origin={originRef.value}
class={[ns.e('tooltip'), tooltipClassName.value]}
position={tooltipPosition.value}
offset={6}
show-arrow
onMouseenter={onOverlayMouseenter}
onMouseleave={onOverlayMouseleave}>
<span>{tooltipContent.value}</span>
</FlexibleOverlay>
</Teleport >
</>
);
},
});
56 changes: 56 additions & 0 deletions packages/devui-vue/devui/data-grid/src/components/grid-icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export function SortIcon(): JSX.Element {
return (
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<circle id="grid-sort-svg-path-1" cx="8" cy="8" r="8"></circle>
<filter x="-34.4%" y="-21.9%" width="168.8%" height="168.8%" filterUnits="objectBoundingBox" id="filter-2">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.085309222 0"
type="matrix"
in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<use fill-rule="evenodd" xlink:href="#grid-sort-svg-path-1"></use>
<polygon points="8 4 11 7 5 7"></polygon>
<polygon points="8 12 5 9 11 9"></polygon>
</g>
</svg>
);
}

export function FilterIcon(): JSX.Element {
return (
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<polygon points="10.0085775 7 10.0085775 15 6 13 6 7 2 3 2 1 14 1 14 3"></polygon>
</g>
</g>
</svg>
);
}

export function ExpandIcon(): JSX.Element {
return (
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect x="0.5" y="0.5" width="15" height="15" rx="2"></rect>
<rect x="4" y="7" width="8" height="2"></rect>
</g>
</svg>
);
}

export function FoldIcon(): JSX.Element {
return (
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect x="0.5" y="0.5" width="15" height="15" rx="2"></rect>
<path d="M8.75,4 L8.75,7.25 L12,7.25 L12,8.75 L8.749,8.75 L8.75,12 L7.25,12 L7.249,8.75 L4,8.75 L4,7.25 L7.25,7.25 L7.25,4 L8.75,4 Z"></path>
</g>
</svg>
);
}
Loading

0 comments on commit 2907c9c

Please sign in to comment.