Skip to content

Commit a9fa673

Browse files
committed
修改横向滚动样式,修改配置文件,修改文档说明
1 parent 96114d3 commit a9fa673

10 files changed

Lines changed: 473 additions & 110 deletions

File tree

packages/docs/.vitepress/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import react from '@vitejs/plugin-react';
2+
import path from 'path';
23
import { defineConfig } from 'vitepress';
34

5+
const workspaceRoot = path.resolve(__dirname, '../../');
6+
47
export default defineConfig({
58
title: 'React Editable Tables',
69
description: 'React 可编辑表格方案集 — 原生轻量版 & Formily 高性能版',
@@ -11,6 +14,13 @@ export default defineConfig({
1114

1215
vite: {
1316
plugins: [react()],
17+
resolve: {
18+
alias: [
19+
{ find: '@react-editable-tables/native/style.css', replacement: path.join(workspaceRoot, 'editable-table/src/components/EditableTable/EditableTable.css') },
20+
{ find: '@react-editable-tables/native', replacement: path.join(workspaceRoot, 'editable-table/src/components/EditableTable/index.tsx') },
21+
{ find: '@react-editable-tables/formily', replacement: path.join(workspaceRoot, 'fast-editable-table/src/index.ts') },
22+
],
23+
},
1424
},
1525

1626
head: [
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
declare module '*.css';
22
declare module '*.vue' {
33
import type { DefineComponent } from 'vue';
4+
45
const component: DefineComponent<object, object, unknown>;
56
export default component;
67
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import EditableTable, { type EditableColumn } from '@react-editable-tables/native';
2+
import { useState } from 'react';
3+
4+
interface Row {
5+
id: string;
6+
name: string;
7+
age: number | undefined;
8+
city: string;
9+
department: string;
10+
email: string;
11+
phone: string;
12+
salary: number | undefined;
13+
status: string;
14+
remark: string;
15+
}
16+
17+
const cities = ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '南京'];
18+
const departments = ['技术部', '产品部', '设计部', '市场部', '运营部', '财务部', '人事部'];
19+
const statusMap: Record<string, string> = { active: '在职', inactive: '离职' };
20+
21+
function generateData(count: number): Row[] {
22+
const names = ['张', '李', '王', '赵', '刘', '陈', '杨', '黄', '周', '吴'];
23+
return Array.from({ length: count }, (_, i) => ({
24+
id: String(i + 1),
25+
name: `${names[i % names.length]}${String.fromCharCode(65 + (i % 26))}`,
26+
age: 22 + (i % 30),
27+
city: cities[i % cities.length],
28+
department: departments[i % departments.length],
29+
email: `user${i + 1}@example.com`,
30+
phone: `138${String(i).padStart(8, '0')}`,
31+
salary: 8000 + (i % 20) * 500,
32+
status: i % 2 === 0 ? 'active' : 'inactive',
33+
remark: i % 3 === 0 ? '重点关注' : '',
34+
}));
35+
}
36+
37+
const data = generateData(500);
38+
39+
const columns: EditableColumn<Row>[] = [
40+
{
41+
title: '姓名',
42+
dataIndex: 'name',
43+
width: 100,
44+
fixed: 'left',
45+
editRender: ({ value, onChange }) => (
46+
<input className="et-editor-input" value={value} onChange={(e) => onChange(e.target.value)} />
47+
),
48+
},
49+
{
50+
title: '年龄',
51+
dataIndex: 'age',
52+
width: 80,
53+
editRender: ({ value, onChange }) => (
54+
<input
55+
className="et-editor-number"
56+
type="number"
57+
value={value ?? ''}
58+
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined)}
59+
/>
60+
),
61+
},
62+
{
63+
title: '城市',
64+
dataIndex: 'city',
65+
width: 100,
66+
editRender: ({ value, onChange }) => (
67+
<select className="et-editor-select" value={value} onChange={(e) => onChange(e.target.value)}>
68+
{cities.map((c) => (
69+
<option key={c} value={c}>
70+
{c}
71+
</option>
72+
))}
73+
</select>
74+
),
75+
},
76+
{
77+
title: '部门',
78+
dataIndex: 'department',
79+
width: 100,
80+
editRender: ({ value, onChange }) => (
81+
<select className="et-editor-select" value={value} onChange={(e) => onChange(e.target.value)}>
82+
{departments.map((d) => (
83+
<option key={d} value={d}>
84+
{d}
85+
</option>
86+
))}
87+
</select>
88+
),
89+
},
90+
{
91+
title: '邮箱',
92+
dataIndex: 'email',
93+
width: 180,
94+
editRender: ({ value, onChange }) => (
95+
<input className="et-editor-input" value={value} onChange={(e) => onChange(e.target.value)} />
96+
),
97+
},
98+
{
99+
title: '手机号',
100+
dataIndex: 'phone',
101+
width: 140,
102+
editRender: ({ value, onChange }) => (
103+
<input className="et-editor-input" value={value} onChange={(e) => onChange(e.target.value)} />
104+
),
105+
},
106+
{
107+
title: '薪资',
108+
dataIndex: 'salary',
109+
width: 100,
110+
editRender: ({ value, onChange }) => (
111+
<input
112+
className="et-editor-number"
113+
type="number"
114+
value={value ?? ''}
115+
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined)}
116+
/>
117+
),
118+
},
119+
{
120+
title: '备注',
121+
dataIndex: 'remark',
122+
width: 120,
123+
editRender: ({ value, onChange }) => (
124+
<input className="et-editor-input" value={value} onChange={(e) => onChange(e.target.value)} />
125+
),
126+
},
127+
{
128+
title: '状态',
129+
dataIndex: 'status',
130+
width: 80,
131+
editRender: ({ value, onChange }) => (
132+
<select className="et-editor-select" value={value} onChange={(e) => onChange(e.target.value)}>
133+
<option value="active">在职</option>
134+
<option value="inactive">离职</option>
135+
</select>
136+
),
137+
render: (value) => statusMap[value] ?? value,
138+
},
139+
{
140+
title: '操作',
141+
dataIndex: 'id',
142+
width: 80,
143+
fixed: 'right',
144+
editable: false,
145+
render: (value) => <a href="javascript:void(0)">详情</a>,
146+
},
147+
];
148+
149+
export default function LargeScrollDemo() {
150+
const [dataSource, setDataSource] = useState(data);
151+
152+
return (
153+
<div>
154+
<p style={{ fontSize: 13, color: '#666', marginBottom: 8 }}>
155+
500 行 × 10 列,虚拟滚动 + 横向滚动,姓名列左侧固定,操作列右侧固定
156+
</p>
157+
<EditableTable<Row>
158+
rowKey="id"
159+
dataSource={dataSource}
160+
onChange={setDataSource}
161+
columns={columns}
162+
onSubmit={(d) => {
163+
console.log('提交数据:', d);
164+
alert(`提交成功!共${d.length}条`);
165+
}}
166+
scrollY={500}
167+
/>
168+
</div>
169+
);
170+
}

packages/docs/guide/getting-started.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ npm install @react-editable-tables/native
2121
pnpm add @react-editable-tables/native
2222
```
2323

24+
> **注意**:Native 方案需要手动导入样式文件,否则表格没有样式:
25+
>
26+
> ```tsx
27+
> import '@react-editable-tables/native/style.css';
28+
> ```
29+
2430
### Formily 方案
2531
2632
```bash
@@ -40,6 +46,7 @@ npm install react react-dom antd
4046
```tsx
4147
import { useState } from 'react';
4248
import EditableTable from '@react-editable-tables/native';
49+
import '@react-editable-tables/native/style.css';
4350

4451
interface User {
4552
id: string;

packages/docs/native/basic.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import BasicDemoSource from '../demos/native/BasicDemo.tsx?raw'
1313
npm install @react-editable-tables/native
1414
```
1515

16+
> **注意**:Native 方案需要手动导入样式文件,否则表格没有样式:
17+
>
18+
> ```tsx
19+
> import '@react-editable-tables/native/style.css';
20+
> ```
21+
1622
## 交互式示例
1723
1824
<ClientOnly>
@@ -24,6 +30,7 @@ npm install @react-editable-tables/native
2430
```tsx
2531
import { useState } from 'react';
2632
import EditableTable from '@react-editable-tables/native';
33+
import '@react-editable-tables/native/style.css';
2734
2835
interface User {
2936
id: string;

packages/docs/native/performance.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,73 @@
33
<script setup>
44
import VirtualScrollDemo from '../demos/native/VirtualScrollDemo.tsx'
55
import VirtualScrollDemoSource from '../demos/native/VirtualScrollDemo.tsx?raw'
6+
import LargeScrollDemo from '../demos/native/LargeScrollDemo.tsx'
7+
import LargeScrollDemoSource from '../demos/native/LargeScrollDemo.tsx?raw'
68
</script>
79

810
EditableTable 基于 `@tanstack/react-virtual` 实现虚拟滚动,只渲染可视区域内的行,即使数据量达到数千行也能保持流畅。
911

1012
## 交互式示例
1113

14+
### 500 行虚拟滚动
15+
16+
<ClientOnly>
17+
<ReactDemo :component="VirtualScrollDemo" :source="VirtualScrollDemoSource" title="500 行虚拟滚动" description="5 列,500 行数据,虚拟滚动只渲染可视区域" />
18+
</ClientOnly>
19+
20+
### 500 行 × 10 列 + 横向滚动
21+
1222
<ClientOnly>
13-
<ReactDemo :component="VirtualScrollDemo" :source="VirtualScrollDemoSource" title="500 行虚拟滚动" description="500 行数据,虚拟滚动只渲染可视区域" />
23+
<ReactDemo :component="LargeScrollDemo" :source="LargeScrollDemoSource" title="10 列横向滚动" description="500 行 × 10 列,虚拟滚动 + 横向滚动,左右列固定" />
1424
</ClientOnly>
1525

26+
## 启用虚拟滚动
27+
28+
传入 `scrollY` 属性即可启用虚拟滚动,值为滚动容器的高度(px):
29+
30+
```tsx
31+
<EditableTable
32+
rowKey="id"
33+
columns={columns}
34+
dataSource={data}
35+
onChange={setData}
36+
scrollY={500} // 启用虚拟滚动,容器高度 500px
37+
/>
38+
```
39+
40+
## 横向滚动
41+
42+
当列数较多、总宽度超出容器时,表格自动支持横向滚动:
43+
44+
- 设置每列的 `width` 属性控制列宽
45+
- 使用 `fixed: 'left' | 'right'` 固定列,固定列在横向滚动时不会移动
46+
- 表头和表体的横向滚动自动同步
47+
48+
```tsx
49+
const columns: EditableColumn<T>[] = [
50+
{
51+
title: '姓名',
52+
dataIndex: 'name',
53+
width: 100,
54+
fixed: 'left', // 左侧固定
55+
editRender: ({ value, onChange }) => (
56+
<input value={value ?? ''} onChange={e => onChange(e.target.value)} />
57+
),
58+
},
59+
// ... 中间的列正常滚动
60+
{
61+
title: '操作',
62+
dataIndex: 'action',
63+
width: 80,
64+
fixed: 'right', // 右侧固定
65+
editable: false,
66+
},
67+
];
68+
```
69+
1670
## 虚拟滚动原理
1771

18-
1. 表格容器设置固定高度(默认 400px
72+
1. 表格容器设置固定高度(通过 `scrollY`
1973
2. 通过 `@tanstack/react-virtual` 计算当前可视区域对应的行索引范围
2074
3. 只渲染可视区域 ± 缓冲区的行
2175
4. 滚动时动态替换渲染的行,保持 DOM 节点数量恒定
@@ -25,3 +79,4 @@ EditableTable 基于 `@tanstack/react-virtual` 实现虚拟滚动,只渲染可
2579
- 虚拟滚动需要表格设置固定高度,通过 CSS 变量 `--et-row-height`(默认 48px)控制行高
2680
- 列固定(`fixed: 'left' | 'right'`)在虚拟滚动下正常工作
2781
- 行编辑模式(`editableMode="row"`)在虚拟滚动下正常工作
82+
- 横向滚动时表头和表体自动同步

packages/editable-table/src/components/EditableTable/EditableTable.css

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,29 @@
3737
border-radius: var(--et-radius);
3838
display: flex;
3939
flex-direction: column;
40-
overflow: hidden;
40+
overflow: hidden; /* 截断内部溢出,避免外层滚动 */
41+
width: 100%; /* 确保表格宽度铺满父容器 */
4142
}
4243

43-
/* ===== 横向滚动容器:表头和表体共享横向滚动 ===== */
44+
/* ===== 表头容器:不产生滚动条,由 JS 通过 translate 同步偏移 ===== */
4445
.et-scroll-x {
45-
overflow-x: auto;
46+
overflow-x: hidden;
4647
overflow-y: hidden;
48+
width: 100%;
4749
}
4850

49-
.et-scroll-x::-webkit-scrollbar {
50-
height: 6px;
51+
/* 有竖向滚动条时,表头保留等宽的不可见滚动条,使内容区宽度与表体一致 */
52+
.et-scroll-x-vscroll {
53+
overflow-y: scroll;
54+
scrollbar-color: transparent transparent;
5155
}
52-
53-
.et-scroll-x::-webkit-scrollbar-thumb {
54-
background: #c1c1c1;
55-
border-radius: 3px;
56+
.et-scroll-x-vscroll::-webkit-scrollbar {
57+
background: transparent;
5658
}
5759

58-
/* ===== 表格内容区:设置最小宽度使列不被压缩 ===== */
60+
/* ===== 表格内容区:列少时撑满容器,列多时允许溢出 ===== */
5961
.et-inner {
62+
width: fit-content;
6063
min-width: 100%;
6164
}
6265

@@ -86,7 +89,7 @@
8689
/* ===== 表体滚动区 ===== */
8790
.et-body {
8891
overflow-y: auto;
89-
overflow-x: hidden;
92+
overflow-x: auto;
9093
}
9194

9295
.et-body::-webkit-scrollbar {
@@ -342,3 +345,14 @@
342345
.et-bordered .et-row:last-child .et-cell {
343346
border-bottom: none;
344347
}
348+
349+
/* ===== 固定列滚动阴影 ===== */
350+
.et-scroll-left .et-fixed-left,
351+
.et-scroll-left .et-header-cell.et-fixed-left {
352+
box-shadow: 6px 0 6px -4px rgba(0, 0, 0, 0.08);
353+
}
354+
355+
.et-scroll-right .et-fixed-right,
356+
.et-scroll-right .et-header-cell.et-fixed-right {
357+
box-shadow: -6px 0 6px -4px rgba(0, 0, 0, 0.08);
358+
}

0 commit comments

Comments
 (0)