0%

介绍

CSS中如何实现继承(扩展)呢?很多编程语言都支持继承(或扩展),在一个基类的基础上添加新的属性或者方法,比如下面是JavaScript中的继承(扩展)语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
constructor() {
this.baseProperty = 'base';
}
baseMethod() {
console.log('base method');
}
}

class Extended extends Base {
constructor() {
super(); // 调用基类的构造函数
this.extendedProperty = 'extended';
}
extendedMethod() {
console.log('extended method');
}
}

通过继承,我们可以把共同的属性或者方法抽象到基类中,然后在子类中添加新的属性或者方法。那么问题来了,CSS中是否也有类似的语法来实现继承(扩展)呢?

原生CSS中的继承

很遗憾,原生CSS并没有提供类似于编程语言中的继承(扩展)语法。早期CSS曾出现过@apply语法,但是现在这个提案已经被废弃了,不推荐使用。

虽然原生CSS没有语法层面的支持,但是我们可以采用多个选择器的方式来实现这种效果,假设我们有三个button,分别时normalwarningerror,这三种button除了颜色不一样之前,其他属性诸如宽度,高度、边框、字体等都是相同的,我们可以这样写:

先定义定义公共的button样式:

1
2
3
4
5
6
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

然后为三种button分别指定颜色:

1
2
3
4
5
6
7
8
9
.normal-button {
background-color: #4caf50; /* Green */
}
.warning-button {
background-color: #ff9800; /* Orange */
}
.error-button {
background-color: #f44336; /* Red */
}

最后使用这些样式的时候,为每个button都指定.button类和他们各自的颜色类:

1
2
3
<button class="button normal-button">Normal</button>
<button class="button warning-button">Warning</button>
<button class="button error-button">Error</button>

这种方法虽然不是正统意义上的继承,但是同样可以达到避免冗余代码的效果。

CSS Module中的继承

CSS Module并不是CSS预处理器,而是一种CSS模块化的方式,它允许我们将CSS样式封装在模块中,并且可以通过导入的方式来使用其他模块的样式。CSS Module的文件名都是以.module.css结尾,比如users.module.css

在CSS Module中,可以使用composes关键字来实现样式的继承(扩展)。假设我们有一个基础的按钮样式button.module.css,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* button.module.css */
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

.normal-button {
composes: button; /* 继承button样式 */
background-color: #4caf50; /* Green */
}

.warning-button {
composes: button; /* 继承button样式 */
background-color: #ff9800; /* Orange */
}

.error-button {
composes: button; /* 继承button样式 */
background-color: #f 44336; /* Red */
}

使用的时候,就无需在指定.button类了,直接使用各自的颜色类即可:

1
2
3
<button class="normal-button">Normal</button>
<button class="warning-button">Warning</button>
<button class="error-button">Error</button>

SaSS中的继承

SaSS(Syntactically Awesome Style Sheets)是一个CSS预处理器,它提供了许多强大的功能,包括变量、嵌套、混合宏(mixins)和继承等。SaSS中的继承可以通过@extend指令来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* base.scss */
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

/* normal.scss */
.normal-button {
@extend .button; /* 继承button样式 */
background-color: #4caf50; /* Green */
}

/* warning.scss */
.warning-button {
@extend .button; /* 继承button样式 */
background-color: #ff9800; /* Orange */
}

/* error.scss */
.error-button {
@extend .button; /* 继承button样式 */
background-color: #f44336; /* Red */
}

使用方法也是一样的:

1
2
3
<button class="normal-button">Normal</button>
<button class="warning-button">Warning</button>
<button class="error-button">Error</button>

Less中的继承

Less是另一个流行的CSS预处理器,它也提供了类似于SaSS的继承功能。Less中的继承可以通过&:extend语法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* base.less */
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

/* normal.less */
.normal-button {
&:extend(.button); /* 继承button样式 */
background-color: #4caf50; /* Green */
}

/* warning.less */
.warning-button {
&:extend(.button); /* 继承button样式 */
background-color: #ff9800; /* Orange */
}

/* error.less */
.error-button {
&:extend(.button); /* 继承button样式 */
background-color: #f44336; /* Red */
}

使用方法同上。

好了,今天就到这里,祝大家编程愉快,我们明天再见,觉得有用,就点个关注吧!

介绍

今天来点简单的,如何在html文件中引入并使用javascript文件,这看起来是一个非常简单的问题,但是对于习惯了使用前端框架的我们来说,其实并不简单,如果大家不查资料,仅凭记忆来写,可能很多人都写不出来。

我们先从简单的开始,先回顾一下如何在HTML中使用普通的javascript文件。

在HTML中使用普通的javascript文件

编写javascript文件

首先我们编写一个简单的javascript文件math.js,文件内容很简单,只包含一个加法函数add,代码如下:

1
2
3
4
// math.js
function add(a, b) {
return a + b;
}

在html文件中引入javascript文件

在同一目录下新建一个文件index.html,然后引入前面定义的javascript文件,并调用add函数,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en-US">
<head>
<script src="math.js"></script>
</head>
<body>
<div id="result"></div>
<script>
const sum = add(1, 2);
document.getElementById("result").innerText = "Sum: " + sum;
</script>
</body>
</html>

解释一下上面的代码:

  1. <head>标签中使用<script src="math.js"></script>引入math.js文件,这样就可以在后续的脚本中使用math.js中定义的函数了。
  2. <body>标签中,我们有一个<div>元素用于显示结果,id为result
  3. <body>标签的最后,我们有一个内联的<script>标签,在这里我们调用了add(1, 2)函数,并将结果显示在result元素中。

用浏览器打开这个html文件,页面会显示Sum: 3
到现在为止,一切正常,现在我们更进一步,采用ES6的模块化语法来重写这个例子。

在HTML中使用ES6模块

我们修改math.js文件,使用ES6的export语法来导出add函数,代码如下:

1
2
3
4
// math.js
export function add(a, b) {
return a + b;
}

此时刷新浏览器,你应该会看到如下错误信息:

1
Uncaught SyntaxError: Unexpected token 'export'

为什么呢?从报错信息来看,浏览器并不认识export这个token,这说明浏览器无法识别ES6模块化语法。
所以我们需要告诉浏览器这是一个模块化的脚本,方法是给<head>中的<script>标签添加type="module"属性,代码如下:

1
2
3
<head>
<script type='module' src="math.js"></script>
</head>

再次刷新浏览器,你会发现还是不行,报错信息变成了:

1
Uncaught ReferenceError: add is not defined

从报错信息来看,浏览器找不到add函数,原因是对于ES6模块导出的方法,我们是不能直接调用的,必须使用import语法来导入,修改index.html文件如下:

1
2
3
4
5
6
7
8
<body>
<div id="result"></div>
<script>
import { add } from "./math.js"; /* 导入add函数 */
const sum = add(1, 2);
document.getElementById("result").innerText = "Sum: " + sum;
</script>
</body>

这次应该没啥问题了吧,再次刷新浏览器,发现还是报错:

1
Uncaught SyntaxError: Cannot use import statement outside a module

为什么呢?
这是因为我们在body中的<script>标签中使用了import语法,但是这个<script>标签并没有指定type="module"属性,所以浏览器认为这是一个普通的脚本,而不是一个模块化的脚本。普通脚本是无法导入ES6模块的。

继续修改index.html文件,给body中的<script>标签添加type="module"属性,代码如下:

1
2
3
4
5
<script type="module">
import { add } from "./math.js";
const sum = add(1, 2);
document.getElementById("result").innerText = "Sum: " + sum;
</script>

再次刷新浏览器,终于不报错了。

怎么样,是不是没有那么简单?我们来总结一下吧:

总结

HTML中使用普通的javascript脚本

  1. 定义普通的javascript文件,直接编写函数。
  2. 在html文件中引入这个javascript文件时,使用<script src="..."></script>标签引入。
  3. 在html文件中调用这个函数时,直接使用函数名调用即可。

HTML中使用ES6模块化脚本

  1. 首先定义ES6模块化的javascript文件,使用export语法导出函数。
  2. 在html文件中引入这个javascript文件时,必须给<script>标签添加type="module"属性。
  3. 在html文件中调用这个模块化的函数时,必须使用import语法导入,并且也要给<script>标签添加type="module"属性。

祝大家编程愉快!觉得有用就点个关注吧!

介绍

今天来点轻松的话题,如何给一个html元素添加tooltip,也就是鼠标悬停时显示一个提示信息。实现tooltip的方式有很多,我们逐一介绍一下。

方式一:使用html的title属性

最简单的方式就是使用html元素的title属性,给一个html元素添加title属性后,鼠标悬停时,浏览器会自动显示title属性的内容作为tooltip。

以下代码为div元素添加了一个tooltip,内容为”This is a tooltip”,当鼠标悬停在div元素上时,会显示这个tooltip。

1
<div title="This is a tooltip">Hover on me</div>
  • 优点:实现简单,不需要额外的css或js代码,浏览器会自动处理tooltip的显示和隐藏。
  • 缺点:tooltip的样式和位置无法自定义,完全依赖浏览器的默认行为。触发速度慢。

方式二: 使用CSS实现tooltip

首先定义元素的HTML结构,以下代码中,外层的div元素作为tooltip的触发区域,内层的span元素作为tooltip的内容。

1
2
3
4
<div class="tooltip">
Hover over me
<span class="tooltip-text">This is a custom tooltip</span>
</div>

然后使用CSS来实现tooltip,代码中有详细的注释,不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
.tooltip {
/*采用相对定位将外层div元素放置在 100px, 100px处,防止tooltip超出窗口*/
position: relative;
top: 100px;
left: 100px;
cursor: pointer; /* 鼠标悬停时显示为手型 */
}

/* tooltip的样式 */
.tooltip .tooltip-text {
visibility: hidden; /* 默认隐藏,hover时才显示 */
width: 120px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
}

/* 这里用来绘制tooltip下面的小三角形 */
.tooltip .tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}

/* hover时显示tooltip */
.tooltip:hover .tooltip-text {
visibility: visible;
}
  • 优点:可以自定义tooltip的样式和位置,灵活性更高。触发速度快。
  • 缺点:需要编写额外的css代码,稍微复杂一些。

方式三:使用JavaScript实现tooltip

这是最灵活的方式了,可以完全自定义tooltip的行为和样式。以下代码中,我们使用JavaScript来动态创建和显示tooltip。

1
2
<div class="js-tooltip">How on me</div>
<div class="tooltip-container hidden">This is a JavaScript tooltip</div>

用js控制tooltip的显示和隐藏:

1
2
3
4
5
6
7
8
9
10
const div = document.querySelector(".js-tooltip");
const tooltip = document.querySelector(".tooltip-container");

div.addEventListener("mouseenter", () => {
tooltip.style.display = "block";
});

div.addEventListener("mouseleave", () => {
tooltip.style.display = "none";
});

用css定义tooltip的样式,这里写得比较简单,大家可以根据需要随意添加。

1
2
3
4
5
6
7
8
9
10
11
.js-tooltip {
margin-top: 32px;
}

.tooltip-container {
position: absolute;
background: #333;
color: white;
padding: 10px;
border-radius: 5px;
}

总结

请以markdown表格的形式比较上述三种方式的优缺点,请添加一列显示速度。

方式 优点 缺点 显示速度
使用title属性 实现简单,不需要额外的css或js代码,浏览器会自动处理tooltip的显示和隐藏。 tooltip的样式和位置无法自定义,完全依赖浏览器的默认行为。
使用CSS实现tooltip 可以自定义tooltip的样式和位置,灵活性更高。 需要编写额外的css代码,稍微复杂一些。
使用JavaScript实现tooltip 最灵活的方式,可以完全自定义tooltip的行为和样式。 需要编写额外的js代码,稍微复杂一些。

今天就到这里了,祝大家编程愉快,我们明天再见,觉得有用,就点个关注吧!

码字不易,在整个互联网充斥着大量AI生成的垃圾内容的今天,还在坚持纯古法手写的人不多, 我Philip是其中一个。

介绍

各位老铁大家好,今天我们来学习一下如何实现可编辑表格,可编辑表格是指在用户点击某个单元格时自动进入编辑状态,用户直接在表格中修改数据,当表格是去焦点时,自动保存修改后的数据。

首先,我们来思考一下,应该如何让表格中的单元格可编辑?如果让你实现这个功能,改如何下手呢?

基本的思路就是,用一个变量控制单元格当前的状态(编辑/非编辑),编辑状态下显示输入框供用户编辑,非编辑状态下显示文本内容。这个思路怎么说呢,大差不差,但是有许多的细节需要完善,我们一步一步来吧。

自定义Table渲染器

Table组件写了好几篇了,但是有一个重要的功能始终没有介绍,那就是自定义渲染器,默认情况下,Ant Design会自动渲染Table的每一行,以及每一行中的每一个Cell,但是有时候我们需要自行渲染行和Cell,比如可编辑单元格就需要自定义渲染器。

Table有一个属性叫做components,这个属性可以用来覆盖默认的行和Cell渲染器。我们可以通过这个属性来实现自定义渲染器。

components属性的定义如下,可以看到其中的headerbody属性分别对应表头和表体的渲染器。而body属性又包含rowcell属性,分别对应表体的行和单元格渲染器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export interface TableComponents<RecordType> {
table?: CustomizeComponent;
header?: {
wrapper?: CustomizeComponent;
row?: CustomizeComponent;
cell?: CustomizeComponent;
};
body?:
| CustomizeScrollBody<RecordType>
| {
wrapper?: CustomizeComponent;
row?: CustomizeComponent;
cell?: CustomizeComponent;
};
}

自定义行渲染器

我们先来实现一个自定义行渲染器,行渲染器最外层用Form包裹,这也这个表格就是一个大的Form,可以方便的进行表单操作。

1
2
3
4
5
6
7
8
const EditableRow: React.FC = ({...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<tr {...props} />
</Form>
);
};

自定义单元格渲染器

接下来我们来实现一个自定义单元格渲染器,单元格渲染器需要根据当前单元格的状态来决定是显示文本还是输入框。

先定义可编辑Cell的属性

1
2
3
4
5
6
7
8
9
interface EditableCellProps {
title: string;
editable: boolean;
children: React.ReactNode;
dataIndex: string;
record: any;
handleSave?: (record: any) => void;
[key: string]: any;
}

然后定义可编辑Cell组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const EditableCell = (props: EditableCellProps) => {
const { title, editable, children, dataIndex, record, handleSave, ...restProps } = props;
const [editing, setEditing] = useState(false);
const inputRef = useRef<InputRef>(null);
const form = Form.useFormInstance();

useEffect(() => {
if (editing) {
inputRef.current?.focus();
}
}, [editing]);

const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};

const save = async () => {
try {
const values = await form.validateFields();

toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};

let childNode = children;

if (editable) {
childNode = editing ? (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[{ required: true, message: `${title} is required.` }]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
style={{ paddingInlineEnd: 24 }}
onClick={toggleEdit}
>
{children}
</div>
);
}

return <td {...restProps}>{childNode}</td>;
};

最后定义一个App来调用我们的自定义行和单元格渲染器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
const App: React.FC = () => {
const [dataSource, setDataSource] = useState<DataType[]>([
{
key: '0',
name: 'Edward King 0',
age: '32',
address: 'London, Park Lane no. 0',
},
{
key: '1',
name: 'Edward King 1',
age: '32',
address: 'London, Park Lane no. 1',
},
]);

const defaultColumns = [
{
title: 'name',
dataIndex: 'name',
width: '30%',
editable: true,
},
{
title: 'age',
dataIndex: 'age',
editable: true,
},
{
title: 'address',
dataIndex: 'address',
editable: true,
},
];

const handleSave = (row: any) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row,
});
setDataSource(newData);
};

const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};

const columns = defaultColumns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record: DataType) => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave,
}),
};
});

return (
<Table
components={components}
bordered
dataSource={dataSource}
columns={columns}
/>
);
};
export default App;

各位老铁大家好,今天我们继续学习Ant Design中的Table组件,修改Table中的内容是一个常见的需求,今天我们来看看如何实现Table中的行编辑功能。我们会在每一行的末尾添加两个按钮,分别是编辑和取消,默认情况下,行数据是只读的,点击编辑按钮时,行数据会变成可编辑模式,点击取消则会恢复为只读模式。

介绍

今天我们继续学习Ant Design的Table组件,今天的主题是如何为Table组件添加行选中功能,也就是用户可以通过点击某一行来选中它,这在很多场景下都非常有用,比如批量删除、批量导出等。关于如何选中行,我们可以使用Table组件提供的rowSelection属性来实现。

实现行选中功能

下面是一个简单的示例,展示了如何在Table组件中实现行选中功能,首先为Table组件添加rowSelection属性,然后在这个属性中定义选中行的逻辑。

1
2
3
4
5
6
<Table
dataSource={tableData}
columns={columns}
rowKey={'id'}
rowSelection={rowSelection} // 添加行选中功能
/>

接下来我们来定义rowSelection的具体内容,rowSelection支持很多回调函数,最常用的就是onChange函数,当我们选中的行数发生变化时(比如选中一行,取消选中一行等),这个函数会被调用。下面的代码使用onChange函数来记录选中的行的key和行数据。

1
2
3
4
5
6
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
console.log('selectedRowKeys changed: ', selectedRowKeys);
console.log('selectedRows changed: ', selectedRows);
},
};

运行程序,选中第一行和第二行,控制台会输出选中的key和行数据,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
selectedRowKeys: 1,2
selectedRows: [
{
"id": 1,
"name": "User1",
"email": "user1@gmail.com",
"phone": "13028296158"
},
{
"id": 2,
"name": "User2",
"email": "user2@gmail.com",
"phone": "13084774670"
}
]

因为默认的rowKey是id,所以selectedRowKeys就是选中行的id:分别是1和2。如果我们在Table组件中设置了rowKeyname

1
2
3
4
5
6
<Table
dataSource={tableData}
columns={columns}
rowKey={'name'}
rowSelection={rowSelection}
/>

那么selectedRowKeys就会变成选中行的name

1
selectedRowKeys: User1,User2

接下来是selectedRows,它是一个数组,包含了所有选中行的数据。我们可以在onChange回调中获取到这些数据,并进行相应的处理。

批量操作

以批量删除为例,我们可以将选中的行数据的id提取出来,然后通过一个删除函数来删除这些数据。

先定义一个state变量来存储选中的行数据的id

1
const [selectedRowKeys, setSelectedRowKeys] = useState([]);

当用户选中某些行时,我们记录下这些行的id

1
2
3
4
5
6
const rowSelection = {
selectedRowKeys,
onChange: (newSelectedRowKeys, selectedRows) => {
setSelectedRowKeys(newSelectedRowKeys);
}
};

最后,当用户点击批量删除按钮时,将这些id传给批量删除函数:

1
2
3
4
5
6
7
8
const handleBatchDelete = () => {
deleteUsers(selectedRowKeys).then(() => {
// 删除成功后,清空选中的行
setSelectedRowKeys([]);
// 重新获取数据
fetchData();
});
}

为什么选中一行,所有行都会被选中?

有时候我们会遇到一个问题,就是当我们选中某一行时,所有行都会被选中。这通常是因为我们没有在

组件中正确设置rowKey属性。rowKey属性用于指定每一行数据的唯一标识符,当我们使用选中功能时,Ant Design会根据这个唯一标识符来判断哪些行是被选中的。我们只要在Table组件中正确设置rowKey属性,就可以避免这个问题。

1
2
3
4
5
6
<Table
dataSource={tableData}
columns={columns}
rowKey={'id'} // 确保设置了唯一的rowKey
rowSelection={rowSelection}
/>

为什么选中了一行,其他行的Checkbox也会被选中?

如果你已经正确设置了rowKey属性,但仍然遇到选中一行时其他行的Checkbox也被选中的问题,这是因为rowKey指定的属性有重复值,比如你设置了rowKey={'id'},但数据中有多行的id是相同的,也会导致重复选中的问题。只要确保每一行数据的rowKey属性是唯一的,就可以避免这个问题。

禁止选中某些行

有些时候,我们可能要禁止用户选中某些行,比如某些用户是管理员或者某些状态的用户不允许被选中。我们可以在rowSelection中添加一个getCheckboxProps函数来实现这个功能。

比如,我们想让name为”User1”的行不能被选中,可以这样实现:

1
2
3
4
5
const rowSelection = {
getCheckboxProps: (record) => ({
disabled: record.name === 'User1', // 这里根据实际情况禁用某些行
}),
};

这样,name为”User1”的行就会被禁用,用户无法选中它。

自定义选中列的名字

有时候,我们需要给选中的列自定义一个名字,比如”选择”,此时可以通过rowSelectioncolumnTitle属性来实现:

1
2
3
const rowSelection = {
columnTitle: '选择',
};

默认情况下,选中列的宽度就是Checkbox的宽度,因为Checkbox的宽度比较小,如果列名较长的话,就会导致该列的标题竖起来,所以我们还需要为这一列设置宽度:

1
2
3
4
const rowSelection = {
columnTitle: '选择',
columnWidth: '50px', // 设置选中列的宽度
};

好了,以上就是今天的内容了,祝大家编程愉快,我们明天再见!

介绍

今天我们学习一下如何在Ant Design的Table组件中使用动态Columns,也就是说列的定义不是固定的,而是根据某些条件动态生成的。这在实际开发中非常常见,比如根据用户权限来决定显示哪些列,或者根据数据的不同类型来显示不同的列。

动态显示/隐藏某些列

考虑下面的需求,我们有一个页面,包含两个tab,一个是我的报告,另一个是所有报告。在我的报告中,我们只需要显示报告的标题和创建时间,而在所有报告中,我们需要显示更多的信息,比如作者等。- 由于我的报告中作者就是我,所以不需要显示作者列。

首先我们定义所有的列,然后根据当前tab的不同来显示或隐藏作者列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const columns = [
{
title: '标题',
dataIndex: 'title',
},
{
title: '创建时间',
dataIndex: 'createdAt',
},
{
title: '作者',
dataIndex: 'author',
},
];

const getColumns = (tab: 'myReports' | 'allReports') => {
if (tab === 'myReports') {
return columns.filter(col => col.dataIndex !== 'author');
}
return columns;
};

return (
<Tabs defaultActiveKey="myReports">
<TabPane tab="我的报告" key="myReports">
<Table columns={getColumns('myReports')} dataSource={myReports} />
</TabPane>
<TabPane tab="所有报告" key="allReports">
<Table columns={getColumns('allReports')} dataSource={allReports} />
</TabPane>
</Tabs>
);

这样,我们就实现的动态列的功能,但是这个方法有一个问题,那就是会产生大量的重复代码,如果两个tab的内容几乎相同,只有作者这一列不同的话,那么我们可以提取一个基类用来渲染tab内容,然后在基类中根据tab的不同来决定显示哪些列。

也就是说,我们只需定义一个columns对象,然后在这个组件中根据传入的tab来决定显示哪些列。这时候我们可以用Columns中的hidden属性来隐藏某些列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ReportTable = ({ tab }: { tab: 'myReports' | 'allReports' }) => {
const columns = [
{
title: '标题',
dataIndex: 'title',
},
{
title: '创建时间',
dataIndex: 'createdAt',
},
{
title: '作者',
dataIndex: 'author',
hidden: tab === 'myReports', // 如果是我的报告,则隐藏作者列
},
];

return <Table columns={columns} dataSource={dataSource} />;
};

这样的实现,是不是更优雅呢?这种方式的好处即使避免大量的重复代码,程序员嘛,怎么能老是重复呢?

今天我们继续讲解Ant Design中的Table组件,今天的主题是如何为Table组件添加操作功能,这里的操作是针对每一行数据而言的,比如我们可以编辑、删除某一行数据,或者查看某一行数据的详情等。

要实现这些操作,我们需要在Table组件的列定义中添加一个操作列(操作列通常位于最后一列),这个操作列通常会包含一些按钮或者菜单,用户可以通过点击这些按钮或菜单来执行相应的操作。

因为操作列并不是数据(Table组件中的dataSource)的一部分,所以这一列并不需要dataIndex属性,我们可以直接在columns数组的末尾添加一个新的列对象,设置其title为“操作”,并使用render渲染具体的操作内容,比如编辑和删除按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const columns = [
// ...
{
title: '操作',
key: 'action',
render: (_: any, record: User) => (
<span>
<a onClick={() => console.log('Edit', record)}>编辑</a>
<span style={{ margin: '0 8px' }}>|</span>
<a onClick={() => console.log('Delete', record)}>删除</a>
</span>
),
},
];

有的时候,因为render函数对应的代码比较多,我们可以单独将其提取出来,作为一个单独的函数来处理,这样可以使代码更清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const renderActions = (record: User) => (
<span>
<a onClick={() => console.log('Edit', record)}>编辑</a>
<span style={{ margin: '0 8px' }}>|</span>
<a onClick={() => console.log('Delete', record)}>删除</a>
</span>
);

const columns = [
// ...
{
title: '操作',
key: 'action',
render: renderActions,
},
];

我们向render函数传入了当前行的数据(record),这样我们就可以在操作中使用这行数据了,上面例子中,我们只是简单地打印了编辑和删除的记录,但在实际应用中,我们可能会调用后台api来执行编辑或删除操作。

由于已经拿到了当前行数据record,所以我们也可以拿到这行数据对应的id,有了id,我们就可以在点击编辑或删除按钮时,将id传递给相应的后台API来做真实的编辑和删除操作了。

最后,我们看一下render函数的定义,它接收三个参数,分别是:

  • value: 当前单元格的值
  • record: 当前行的数据记录
  • index: 当前行的索引
    在我们的例子中,我们并没有使用valueindex,但它们在某些情况下可能会有用,比如当你需要根据行索引来处理某些逻辑时。
1
render?: (value: any, record: RecordType, index: number) => React.ReactNode | RenderedCell<RecordType>;

render函数的返回值可以是一个React节点(比如一个按钮或链接),也可以是一个自定义的渲染组件。目前我们用到的都是第一种情况,返回一个React节点。

下面我们以编辑和删除为例,看看如何通过Actions列处理这两种操作。

定义Action列

首先,我们在Action列中定义两个按钮:编辑和删除,当用户点击这两个按钮时,分别执行对应的操作。为了显示美观,我们使用Space组件来排列这两个按钮。Space组件会自动在两个组件之间加上间距。

1
2
3
4
5
6
7
8
const renderAction = (value: any, record: User) => {
return (
<Space>
<Button type='link' onClick={() => onEditButtonClick(record)}>编辑</Button>
<Button type='link' onClick={() => onDeleteButtonClick(record)}>删除</Button>
</Space>
)
}

删除操作

这里使用onDeleteButtonClick函数来处理删除操作。删除操作是一个比较敏感的操作,通常来说,在执行删除操作之前,我们需要弹出一个确认对话框,询问用户是否真的要删除这个记录。也就是要给用户一个二次确认的机会。这里我们使用Ant Design的Modal组件来实现这个功能。

1
2
3
4
5
6
7
8
9
10
11
12
const onDeleteButtonClick = (record: User) => {
Modal.confirm({
title: '确认删除',
content: `您确定要删除用户 ${record.name} 吗?`,
onOk: () => {
handleDelete(record.id);
},
onCancel: () => {
console.log('Delete cancelled');
},
})
}

只有当用户点击了Modal上的“删除”按钮时,才会真正执行删除操作。

调用API删除数据

1
2
3
4
5
6
7
8
9
10
const handleDelete = async (userId: number) => {
try {
// 调用后端API删除用户
await deleteUser(userId);
// 删除成功后重新获取用户列表
getUsers(pageNumber, pageSize);
} catch (error) {
console.error('Error deleting user:', error);
}
}

删除的操作就比较简单了,我们调用后端API deleteUser,传入用户的ID,然后在删除成功后重新获取用户列表。

编辑操作

编辑操作一般通过弹窗的形式来实现,在Ant Design中,弹窗就是Modal组件。我们可以在点击编辑按钮时,弹出一个Modal,里面包含一个表单,用户可以在这个表单中修改用户信息。

定义编辑Modal

首先定义这个Modal的属性,注意:Modal的打开和关闭需要父控件来控制,所以我们传入一个变量isVisible来控制Modal的显示和隐藏。另外,我们还需要传入一个userId来表示当前编辑的用户ID(用来获取用户详情并显示在Modal上),以及两个回调函数onOKonCancel,分别在编辑成功或者取消编辑时调用。

1
2
3
4
5
6
type UserEditModalProps = {
isVisible: boolean; // 控制Modal的打开和关闭
userId: number | null; // 当前编辑的用户ID
onOK: () => void;
onCancel: () => void;
}

紧接着定义Modal组件,组件的命名规则一般是:业务名称 + 操作名称 + 组件类型,所以我们这里就命名为UserEditModal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default function UserEditModal(props: UserEditModalProps) {
const [form] = Form.useForm();
const { isVisible, userId, onOK, onCancel } = props;

return (
<Modal
title="编辑用户"
open={isVisible}
okText={'保存'}
onOk={onOkButtonClick}
onCancel={onCancel}
cancelText={'取消'}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item
name="email"
label="邮箱"
rules={[{ required: true, message: '请输入邮箱' }]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
{/* 可以添加更多表单项 */}
</Form>
</Modal>
)
}

因为我们这里使用了Form组件,所以需要先创建一个form实例,这个实例会被传递给Modal中的Form组件。接下来,我们需要在Modal中添加表单项,比如姓名和邮箱等。

接下来我们通过传入的userId来获取用户详情,并将其设置到表单中,这样用户在编辑时就可以看到当前用户的信息了。

1
2
3
4
5
6
useEffect(() => {
if (isVisible && userId) {
// 这里可以添加获取用户信息的逻辑
getUserInfo(userId);
}
}, [isVisible, userId]);

这里需要注意的是,Ant Design的Modal组件无论打开还是关闭,都会渲染到页面上,为了避免在Modal打开之前就调用API,我们这里加了isVisible的判断,只有当Modal是可见的,并且userId存在时,才会调用getUserInfo函数来获取用户信息。

当成功获取用户信息后,通过form.setFieldsValue将这些信息显示到页面上。

1
2
3
4
const getUserInfo = async (userId: number) => {
const userDetail = await getUserDetail(userId);
form.setFieldsValue(userDetail);
}

接下来就是编辑操作了,当用户点击Modal的保存按钮时,我们需要验证表单数据是否有效,如果有效,就调用API来保存用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const submitForm = async (values: any) => {
// 调用API保存用户信息
const res = await submitUser(values);
if (res.success) {
onOK(); // 调用父组件的 onOK 方法,关闭Modal
} else {
console.error('保存失败:', res.message);
}
}

const onOkButtonClick = () => {
// 验证表单数据
form.validateFields().then(values => {
submitForm(values)
}).catch(error => {
console.error('表单验证失败:', error);
});
}

如果表单数据验证失败,比如某些必填项没有填写,或者格式不正确,validateFields会抛出一个错误,Ant Design也会在页面上给出相应的提示。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export default function UserEditModal(props: UserEditModalProps) {
const [form] = Form.useForm();
const { isVisible, userId, onOK, onCancel } = props;

const getUserInfo = async (userId: number) => {
const userDetail = await getUserDetail(userId);
form.setFieldsValue(userDetail);
}

const submitForm = async (values: any) => {
const res = await submitUser(values);
if (res.success) {
onOK(); // 调用父组件的 onOK 方法,关闭Modal
} else {
// 处理错误
console.error('保存失败:', res.message);
}
}

const onOkButtonClick = () => {
form.validateFields().then(values => {
// 这里可以添加保存用户信息的逻辑
submitForm(values)
})
}

useEffect(() => {
if (isVisible && userId) {
getUserInfo(userId);
}
}, [isVisible, userId]);

return (
<Modal
title="编辑用户"
open={isVisible}
okText={'保存'}
onOk={onOkButtonClick}
onCancel={onCancel}
cancelText={'取消'}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item
name="email"
label="邮箱"
rules={[{ required: true, message: '请输入邮箱' }]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
</Form>
</Modal>
)
}

好了,今天就到这里了,我们明天再见!

介绍

大家好,今天我们继续讲解Ant Design中的Table组件,排序和搜索是数据表格中非常常见的功能,今天我们来看看如何为Table组件添加这两个功能。

在阅读本篇之前,还是建议先阅读前面的几篇文章,了解Table组件的基本用法。

为Table组件添加排序功能

排序功能的添加非常简单,只需要在列的定义中添加sorter属性即可。我们以用户ID列为例,给它添加一个排序功能。sorter属性对应一个排序函数,这个函数接受两个参数,分别是要比较的两行数据,返回值决定了如何排序这两行数据。

比如下面的代码按照用户ID升序排列

1
2
3
4
5
6
7
8
9
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: (a: User, b: User) => a.id - b.id,
},
// ...
];

排序的方式取决于a.id - b.id的结果:

  • 如果为负数,表示a的ID小于b的ID,a会排在b前面;
  • 如果为正数,表示a的ID大于b的ID,a会排在b后面;
  • 如果为0,表示两者相等,顺序不变。

设置好排序之后,表头对应的字段右侧会出现两个小箭头,点击上面的箭头表示升序排列,点击下面的箭头表示降序排列。

降序排列

默认的排序规则是升序排列,如果要按照用户ID降序排列,只需要把a.id - b.id改成b.id - a.id即可。

1
2
3
4
5
6
7
8
9
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: (a: User, b: User) => b.id - a.id,
},
// ...
];

默认的排序顺序

如果要设置默认的排序顺序,可以使用defaultSortOrder属性。这个属性可以设置为'ascend'表示升序,或者'descend'表示降序。

1
2
3
4
5
6
7
8
9
10
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: (a: User, b: User) => a.id - b.id,
defaultSortOrder: 'descend', // 默认降序排列
},
// ...
];

为Table组件添加搜索功能

搜索功能是通过在列定义中添加filter属性来实现的。我们以用户姓名列为例,给它添加一个搜索功能。filter属性对应一个过滤函数,这个函数接受一个参数,表示要过滤的行数据,返回值为true表示保留该行数据,返回值为false表示过滤掉该行数据。

比如下面的代码按照用户姓名进行搜索

1
2
3
4
5
6
7
8
9
10
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
filters: names, // 这里names需要提前设置一下
onFilter: (value: string, record: User) => record.name.includes(value),
},
// ...
];

可以看到,我们给姓名列添加了filters属性,对应的值是一个数组,数组中每个对象包含textvalue两个属性,分别表示过滤值的显示文本和实际值。- 当我们点击过滤按钮时,弹出的下拉框会显示这些过滤值供用户选择,我们可以在调用后端API成功之后构建这个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const [names, setNames] = useState<{ text: string; value: string }[]>([]);

const getUsers = async (pageNumber: number, pageSize?: number) => {
try {
setLoading(true);
const res = await fetchUsers(pageNumber, pageSize);
setTableData(res.data);
setTotal(res.total);
setNames(res.data.map((user) => ({ text: user.name, value: user.name }))); // 设置names
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setLoading(false);
}
};

下面在来看看onFilter属性,这个属性是一个函数,接受两个参数,第一个参数是过滤值,第二个参数是要过滤的行数据。这个函数返回true表示保留该行数据,返回false表示过滤掉该行数据。- 也就是说onFilter函数是用来完成过滤操作的。

它的实现很简单,只要行数据包含对应的过滤值,我们就保留这一行。

1
onFilter: (value: string, record: User) => record.name.includes(value),

现在让我们运行程序,一起看看效果吧,如下图。

alt text

今天就到这里了,我们明天再见!

昨天下了一夜雨,今天很凉快。雨后的空气格外清新,感觉心情也变好了。希望大家在学习的过程中也能保持好心情,享受编程的乐趣。

介绍

这篇文章我们继续讲解Ant Design中的Table组件,今天我们来看看如何为Table组件添加分页功能。随着项目的进行,数据量会变得越来越大,一页不可能显示所有的数据,所以分页功能就非常必要了。想了解之前的内容,可以先看看下面这几篇文章:

为Table组件添加分页功能

为了实现分页,我们首先模拟一些数据,假设后端API返回100条数据,我们改造一下fetchUsers这个方法,动态生成100条User数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export async function fetchUsers() {
const users: User[] = Array.from({ length: 100 }, (_, index) => ({
id: index + 1,
name: `User${index + 1}`,
email: `user${index + 1}@gmail.com`,
phone: `130${Math.floor(10000000 + Math.random() * 90000000)}`,
}));

return new Promise<{ data: User[] }>((resolve) => {
setTimeout(() => {
resolve({ data: users });
}, 1000); // 1秒后返回数据
});
}

现在运行程序,发现分页功能已经自动启用了,这是因为Ant Design的Table组件默认会启用分页功能。但是这个分页实际上是纯前端的分页,也就是说,只调用了一次后端API返回所有数据,这样做的缺点是如果数据量非常大,前端加载时间会很长,用户体验会很差。

Table组件默认启用分页功能, 其默认配置为:pageSize=10, showSizeChanger=true。

所以,真正的分页一定需要后端api的支持,当切换页码时,前端只需要传递当前页码和每页条数给后端,后端返回对应的数据即可。

前端分页:一次性获取全部数据,前端进行分页渲染。
后端分页:每次请求只获取当前页数据,后端返回对应结果。
小技巧:切换页码时若调用API是后端分页,否则为前端分页。

我们改造一下fetchUsers方法,模拟后端分页的api。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { User } from "./User";

export async function fetchUsers(pageNumber: number, pageSize: number = 10): Promise<{ data: User[] }> {
// 动态生成100条数据
const users: User[] = Array.from({ length: 100 }, (_, index) => ({
id: index + 1,
name: `User${index + 1}`,
email: `user${index + 1}@gmail.com`,
phone: `130${Math.floor(10000000 + Math.random() * 90000000)}`,
}));

// 获取分页对应的数据
const pageData = users.slice((pageNumber - 1) * pageSize, pageNumber * pageSize);

// 返回分页数据
return new Promise<{ data: User[] }>((resolve) => {
setTimeout(() => {
resolve({ data: pageData });
}, 1000); // 1秒后返回数据
});
}

然后改造一下getUsers方法,传递页码和每页条数给fetchUsers方法。

1
2
3
4
5
6
7
8
9
10
11
const getUsers = async (pageNumber: number, pageSize?: number) => {
try {
setLoading(true);
const res = await fetchUsers(pageNumber, pageSize);
setTableData(res.data);
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setLoading(false);
}
};

接下来在useEffect中调用getUsers方法时,传递页码1。

1
2
3
useEffect(() => {
getUsers(1);
}, []);

这时候,你会发现Table组件右下角没有分页了,只显示了一个页码1,这是因为我们只返回了一页数据,要开启分页功能必须返回一个total参数告知Table组件数据的总数。

我们在fetchUsers方法的返回值中添加一个total参数,表示数据的总数。

1
2
3
4
5
6
7
8
9
export async function fetchUsers(...): {
// ...
return new Promise<FetchUsersResponse>((resolve) => {
setTimeout(() => {
const response: FetchUsersResponse = { data: pageData, total: users.length }; // 添加total属性
resolve(response);
}, 1000);
});
}

在Users页面,我们需要定一个state来保存total

1
const [total, setTotal] = useState(0); // 总数据条数

然后在获取数据时,设置total

1
2
3
4
5
6
7
8
9
const getUsers = async (pageNumber: number, pageSize?: number) => {
try {
setLoading(true);
const res = await fetchUsers(pageNumber, pageSize);
setTableData(res.data);
setTotal(res.total); // 设置总数据条数
}
// ...
};

最后,在Table组件中传递total属性,并响应onChange函数,在用户切换页面时,调用API获取对应的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
<Table
loading={loading}
dataSource={tableData}
columns={columns}
rowKey={'id'}
pagination={{
pageSize: 10,
total: total,
onChange: (page) => {
getUsers(page);
},
}}
/>

此时重新运行程序,然后点击不同的页码,你会发现数据会根据页码变化而变化,这样分页功能就实现了。但是当你改变每页显示的条数时,你会发现数据并没有变化,这是因为我们在onChange函数中没有传递每页条数,而是将每页条数固定为10条了,我们需要改造一下onChange函数,传递每页条数。

我们可以添加一个新函数来响应Table组件的onChange事件。

1
2
3
4
const onPageChange = (pageNumber: number, pageSize: number) => {
setPageNumber(pageNumber);
setPageSize(pageSize);
};

然后将pageNumberpageSize保存到state中。

1
2
const [pageNumber, setPageNumber] = useState(1); // 当前页码
const [pageSize, setPageSize] = useState(10); // 每页条数

并更新useEffect,将pageNumberpageSize作为依赖变量,这样当他们变化时,重新调用API获取数据。

1
2
3
useEffect(() => {
getUsers(pageNumber, pageSize);
}, [pageNumber, pageSize]);

最后在Table组件中传递onPageChange函数。

1
2
3
4
5
6
7
8
9
10
<Table
loading={loading}
dataSource={tableData}
columns={columns}
rowKey={'id'}
pagination={{
total: total,
onChange: onPageChange,
}}
/>

再次刷新页面,一切正常,分页功能完美实现。以上就是为Ant Design的Table组件添加分页功能的完整步骤。Table组件的分页功能非常强大,支持很多配置向,以上只是冰山一角,你可以查看Ant Design官方文档了解更多配置选项。下面是一个比较完整的配置示例。

1
2
3
4
5
6
7
8
9
pagination={{
current: pageNumber,
pageSize: pageSize,
total: total,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} items`,
onChange: onPageChange,
}}

禁用sizeChanger

如果你不希望用户改变每页显示的条数,可以禁用sizeChanger功能,只需要在pagination属性中添加showSizeChanger: false即可。

1
2
3
4
5
6
7
8
9
<Table
loading={loading}
dataSource={tableData}
columns={columns}
rowKey={'id'}
pagination={{
showSizeChanger: false, // 禁用每页条数选择器
}}
/>

禁用分页

如果你十分确定,你的数据一页就能展示完,比如10条以内的数据,那么你可以禁用分页功能。只需要在Table组件中添加pagination={false}属性即可。

1
2
3
4
5
6
7
<Table
loading={loading}
dataSource={tableData}
columns={columns}
rowKey={'id'}
pagination={false} // 禁用分页
/>

今天就到这里了,祝大家编程愉快,我们明天见!觉得有用,就点个关注吧!