Skip to content

Commit 2907c9c

Browse files
authored
新增支持虚拟滚动的表格组件 (#1796)
1 parent e89da31 commit 2907c9c

30 files changed

+5131
-0
lines changed

packages/devui-vue/devui-cli/templates/vue-devui.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { App } from 'vue';
2525
2626
${imports.join('\n')}
2727
import './style/devui.scss';
28+
import './style/index.scss';
2829
2930
const installs = [
3031
${installs.join(',\n ')}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { App } from "vue";
2+
import DataGrid from './src/data-grid';
3+
4+
export * from './src/data-grid-types';
5+
export { DataGrid }
6+
7+
export default {
8+
title: 'DataGrid 数据表格',
9+
category: '数据展示',
10+
status: '100%',
11+
install(app: App): void {
12+
app.component(DataGrid.name, DataGrid);
13+
}
14+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { defineComponent, inject, ref, watch, onMounted, onBeforeMount } from 'vue';
2+
import { useNamespace } from '../../../shared/hooks/use-namespace';
3+
import GridHead from './grid-head';
4+
import GridBody from './grid-body';
5+
import { DataGridInjectionKey } from '../data-grid-types';
6+
import type { DataGridContext } from '../data-grid-types';
7+
import { useDataGridLazy } from '../composables/use-data-grid-scroll';
8+
9+
export default defineComponent({
10+
name: 'FixHeadGrid',
11+
setup() {
12+
const ns = useNamespace('data-grid');
13+
const {
14+
scrollRef,
15+
headBoxRef,
16+
showHeader,
17+
bodyContentWidth,
18+
bodyContentHeight,
19+
renderColumnData,
20+
renderFixedLeftColumnData,
21+
renderFixedRightColumnData,
22+
renderRowData,
23+
translateX,
24+
translateY,
25+
bodyScrollLeft,
26+
rootCtx,
27+
} = inject(DataGridInjectionKey) as DataGridContext;
28+
const hasScrollbar = ref(false);
29+
let resizeObserver: ResizeObserver;
30+
useDataGridLazy(scrollRef);
31+
32+
const isHaveScrollbar = () => {
33+
if (scrollRef.value) {
34+
hasScrollbar.value = scrollRef.value.scrollHeight > scrollRef.value.clientHeight;
35+
}
36+
};
37+
38+
watch(bodyContentHeight, isHaveScrollbar, { immediate: true });
39+
40+
onMounted(() => {
41+
if (scrollRef.value) {
42+
resizeObserver = new ResizeObserver(isHaveScrollbar);
43+
resizeObserver.observe(scrollRef.value);
44+
}
45+
});
46+
47+
onBeforeMount(() => {
48+
resizeObserver?.disconnect();
49+
});
50+
51+
return () => (
52+
<div>
53+
{showHeader.value && (
54+
<div ref={headBoxRef} class={ns.e('head-wrapper')} style={{ 'overflow-y': hasScrollbar.value ? 'scroll' : 'auto' }}>
55+
<div class={ns.e('x-space')} style={{ width: bodyContentWidth.value + 'px' }}></div>
56+
<GridHead
57+
columnData={renderColumnData.value}
58+
leftColumnData={renderFixedLeftColumnData.value}
59+
rightColumnData={renderFixedRightColumnData.value}
60+
translateX={translateX.value}
61+
bodyScrollLeft={bodyScrollLeft.value}
62+
/>
63+
</div>
64+
)}
65+
<div ref={scrollRef} class={[ns.e('body-wrapper'), 'devui-scroll-overlay']}>
66+
<div class={ns.e('x-space')} style={{ width: bodyContentWidth.value + 'px' }}></div>
67+
<div class={ns.e('y-space')} style={{ height: bodyContentHeight.value + 'px' }}></div>
68+
69+
{Boolean(renderRowData.value.length) ? (
70+
<GridBody
71+
rowData={renderRowData.value}
72+
columnData={renderColumnData.value}
73+
leftColumnData={renderFixedLeftColumnData.value}
74+
rightColumnData={renderFixedRightColumnData.value}
75+
translateX={translateX.value}
76+
translateY={translateY.value}
77+
bodyScrollLeft={bodyScrollLeft.value}
78+
/>
79+
) : (
80+
<div class={ns.e('empty')} style={{ left: bodyScrollLeft.value + 'px' }}>
81+
{rootCtx.slots.empty?.()}
82+
</div>
83+
)}
84+
</div>
85+
</div>
86+
);
87+
},
88+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { defineComponent, toRefs, inject, ref, Teleport } from 'vue';
2+
import { FlexibleOverlay } from '../../../overlay';
3+
import GridTd from './grid-td';
4+
import { gridBodyProps, DataGridInjectionKey } from '../data-grid-types';
5+
import type { GridBodyProps, DataGridContext, InnerRowData } from '../data-grid-types';
6+
import { useNamespace } from '../../../shared/hooks/use-namespace';
7+
import { useOverflowTooltip } from '../composables/use-overflow-tooltip';
8+
import { ToggleTreeIcon, DataGridCheckboxClass } from '../const';
9+
10+
export default defineComponent({
11+
name: 'GridBody',
12+
props: gridBodyProps,
13+
setup(props: GridBodyProps) {
14+
const ns = useNamespace('data-grid');
15+
const { rowClass, rootCtx } = inject(DataGridInjectionKey) as DataGridContext;
16+
const { rowData, columnData, leftColumnData, rightColumnData, translateX, translateY, bodyScrollLeft } = toRefs(props);
17+
const currentRowIndex = ref<number | undefined>();
18+
const {
19+
showTooltip,
20+
originRef,
21+
tooltipContent,
22+
tooltipPosition,
23+
tooltipClassName,
24+
onCellMouseenter,
25+
onCellMouseleave,
26+
onOverlayMouseenter,
27+
onOverlayMouseleave
28+
} = useOverflowTooltip();
29+
const trClasses = (rowData: InnerRowData, rowIndex: number) => {
30+
const realRowClass = typeof rowClass.value === 'string' ? rowClass.value : rowClass.value(rowData, rowIndex);
31+
return {
32+
[ns.e('tr')]: true,
33+
[realRowClass]: true,
34+
'hover-tr': currentRowIndex.value === rowIndex,
35+
};
36+
};
37+
const onRowClick = (e: Event, rowData: InnerRowData, rowIndex: number) => {
38+
const composedPath = e.composedPath() as HTMLElement[];
39+
if (composedPath.some((item) => item.classList?.contains(ToggleTreeIcon) || item.classList?.contains(DataGridCheckboxClass))) {
40+
return;
41+
}
42+
rootCtx.emit('rowClick', { row: { ...rowData }, renderRowIndex: rowIndex, flattenRowIndex: rowData.$rowIndex });
43+
};
44+
const onTrMouseenterOrLeave = (rowIndex: number | undefined) => {
45+
currentRowIndex.value = rowIndex;
46+
};
47+
48+
return () => (
49+
<>
50+
{Boolean(leftColumnData.value.length) && (
51+
<div
52+
class={ns.e('sticky-left-body')}
53+
style={{ left: bodyScrollLeft.value + 'px', transform: `translateY(${translateY.value}px)` }}>
54+
{rowData.value.map((itemRow, rowIndex) => (
55+
<div
56+
class={trClasses(itemRow, rowIndex)}
57+
onClick={(e) => onRowClick(e, itemRow, rowIndex)}
58+
onMouseenter={() => onTrMouseenterOrLeave(rowIndex)}
59+
onMouseleave={() => onTrMouseenterOrLeave(undefined)}>
60+
{leftColumnData.value.map((cellData, cellIndex) => (
61+
<GridTd
62+
class={{ [ns.e('last-sticky-left-cell')]: cellIndex === leftColumnData.value.length - 1 }}
63+
rowData={itemRow}
64+
cellData={cellData}
65+
rowIndex={rowIndex}
66+
mouseenterCb={onCellMouseenter}
67+
mouseleaveCb={onCellMouseleave}
68+
/>
69+
))}
70+
</div>
71+
))}
72+
</div>
73+
)}
74+
75+
{Boolean(rightColumnData.value.length) && (
76+
<div
77+
class={ns.e('sticky-right-body')}
78+
style={{ right: `-${bodyScrollLeft.value}px`, transform: `translateY(${translateY.value}px)` }}>
79+
{rowData.value.map((itemRow, rowIndex) => (
80+
<div
81+
class={trClasses(itemRow, rowIndex)}
82+
onClick={(e) => onRowClick(e, itemRow, rowIndex)}
83+
onMouseenter={() => onTrMouseenterOrLeave(rowIndex)}
84+
onMouseleave={() => onTrMouseenterOrLeave(undefined)}>
85+
{rightColumnData.value.map((cellData, cellIndex) => (
86+
<GridTd
87+
class={{ [ns.e('first-sticky-right-cell')]: cellIndex === 0 }}
88+
rowData={itemRow}
89+
cellData={cellData}
90+
rowIndex={rowIndex}
91+
mouseenterCb={onCellMouseenter}
92+
mouseleaveCb={onCellMouseleave}
93+
/>
94+
))}
95+
</div>
96+
))}
97+
</div>
98+
)}
99+
100+
<div class={ns.e('body')} style={{ transform: `translate(${translateX.value}px, ${translateY.value}px)` }}>
101+
{rowData.value.map((itemRow, rowIndex) => (
102+
<div
103+
class={trClasses(itemRow, rowIndex)}
104+
onClick={(e) => onRowClick(e, itemRow, rowIndex)}
105+
onMouseenter={() => onTrMouseenterOrLeave(rowIndex)}
106+
onMouseleave={() => onTrMouseenterOrLeave(undefined)}>
107+
{columnData.value.map((cellData) => (
108+
<GridTd
109+
rowData={itemRow}
110+
cellData={cellData}
111+
rowIndex={rowIndex}
112+
mouseenterCb={onCellMouseenter}
113+
mouseleaveCb={onCellMouseleave}
114+
/>
115+
))}
116+
</div>
117+
))}
118+
</div>
119+
<Teleport to='body'>
120+
<FlexibleOverlay
121+
v-model={showTooltip.value}
122+
origin={originRef.value}
123+
class={[ns.e('tooltip'), tooltipClassName.value]}
124+
position={tooltipPosition.value}
125+
offset={6}
126+
show-arrow
127+
onMouseenter={onOverlayMouseenter}
128+
onMouseleave={onOverlayMouseleave}>
129+
<span>{tooltipContent.value}</span>
130+
</FlexibleOverlay>
131+
</Teleport>
132+
</>
133+
);
134+
},
135+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { defineComponent, toRefs, Teleport } from 'vue';
2+
import { useNamespace } from '../../../shared/hooks/use-namespace';
3+
import { FlexibleOverlay } from '../../../overlay';
4+
import GridTh from './grid-th';
5+
import { gridHeadProps } from '../data-grid-types';
6+
import type { GridHeadProps } from '../data-grid-types';
7+
import { useOverflowTooltip } from '../composables/use-overflow-tooltip';
8+
9+
export default defineComponent({
10+
name: 'GridHead',
11+
props: gridHeadProps,
12+
setup(props: GridHeadProps) {
13+
const ns = useNamespace('data-grid');
14+
const { columnData, leftColumnData, rightColumnData, translateX, bodyScrollLeft } = toRefs(props);
15+
const {
16+
showTooltip,
17+
originRef,
18+
tooltipContent,
19+
tooltipPosition,
20+
tooltipClassName,
21+
onCellMouseenter,
22+
onCellMouseleave,
23+
onOverlayMouseenter,
24+
onOverlayMouseleave
25+
} = useOverflowTooltip();
26+
27+
return () => (
28+
<>
29+
{Boolean(leftColumnData.value.length) && (
30+
<div class={[ns.e('head'), ns.e('sticky-left-head')]} style={{ left: bodyScrollLeft.value + 'px' }}>
31+
{leftColumnData.value.map((item, index) => (
32+
<GridTh
33+
columnConfig={item}
34+
class={{ [ns.e('last-sticky-left-cell')]: index === leftColumnData.value.length - 1 }}
35+
mouseenterCb={onCellMouseenter}
36+
mouseleaveCb={onCellMouseleave}
37+
/>
38+
))}
39+
</div>
40+
)}
41+
42+
{Boolean(rightColumnData.value.length) && (
43+
<div class={[ns.e('head'), ns.e('sticky-right-head')]} style={{ right: `-${bodyScrollLeft.value}px` }}>
44+
{rightColumnData.value.map((item, index) => (
45+
<GridTh
46+
columnConfig={item}
47+
class={{ [ns.e('first-sticky-right-cell')]: index === 0 }}
48+
mouseenterCb={onCellMouseenter}
49+
mouseleaveCb={onCellMouseleave}
50+
/>
51+
))}
52+
</div>
53+
)}
54+
55+
<div class={ns.e('head')} style={{ transform: `translate(${translateX.value}px,0)` }}>
56+
{columnData.value.map((item) => (
57+
<GridTh columnConfig={item} mouseenterCb={onCellMouseenter} mouseleaveCb={onCellMouseleave} />
58+
))}
59+
</div>
60+
61+
<Teleport to='body'>
62+
<FlexibleOverlay
63+
v-model={showTooltip.value}
64+
origin={originRef.value}
65+
class={[ns.e('tooltip'), tooltipClassName.value]}
66+
position={tooltipPosition.value}
67+
offset={6}
68+
show-arrow
69+
onMouseenter={onOverlayMouseenter}
70+
onMouseleave={onOverlayMouseleave}>
71+
<span>{tooltipContent.value}</span>
72+
</FlexibleOverlay>
73+
</Teleport >
74+
</>
75+
);
76+
},
77+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export function SortIcon(): JSX.Element {
2+
return (
3+
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
4+
<defs>
5+
<circle id="grid-sort-svg-path-1" cx="8" cy="8" r="8"></circle>
6+
<filter x="-34.4%" y="-21.9%" width="168.8%" height="168.8%" filterUnits="objectBoundingBox" id="filter-2">
7+
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
8+
<feGaussianBlur stdDeviation="1.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
9+
<feColorMatrix
10+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.085309222 0"
11+
type="matrix"
12+
in="shadowBlurOuter1"></feColorMatrix>
13+
</filter>
14+
</defs>
15+
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
16+
<use fill-rule="evenodd" xlink:href="#grid-sort-svg-path-1"></use>
17+
<polygon points="8 4 11 7 5 7"></polygon>
18+
<polygon points="8 12 5 9 11 9"></polygon>
19+
</g>
20+
</svg>
21+
);
22+
}
23+
24+
export function FilterIcon(): JSX.Element {
25+
return (
26+
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
27+
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
28+
<g>
29+
<polygon points="10.0085775 7 10.0085775 15 6 13 6 7 2 3 2 1 14 1 14 3"></polygon>
30+
</g>
31+
</g>
32+
</svg>
33+
);
34+
}
35+
36+
export function ExpandIcon(): JSX.Element {
37+
return (
38+
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
39+
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
40+
<rect x="0.5" y="0.5" width="15" height="15" rx="2"></rect>
41+
<rect x="4" y="7" width="8" height="2"></rect>
42+
</g>
43+
</svg>
44+
);
45+
}
46+
47+
export function FoldIcon(): JSX.Element {
48+
return (
49+
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
50+
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
51+
<rect x="0.5" y="0.5" width="15" height="15" rx="2"></rect>
52+
<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>
53+
</g>
54+
</svg>
55+
);
56+
}

0 commit comments

Comments
 (0)