前言
在现代前端开发中,组件化是提高代码复用性和维护性的关键。本文将介绍如何基于 Ant Design 的 Descriptions 组件,结合 React 和 TypeScript,参考 Vben Admin 的设计思想,封装一个功能完善、灵活易用的描述列表组件。
组件设计思路
需求分析
描述列表(Description List)通常用于展示对象的详细信息,如用户资料、订单详情等。一个高质量的描述列表组件需要具备以下特性:
-
支持动态配置描述项
-
支持自定义样式
-
支持条件渲染
-
提供实例方法用于动态更新
-
完善的类型定义
技术选型
-
UI 基础:Ant Design 的 Descriptions 组件
-
框架:React 18
-
类型系统:TypeScript
-
设计参考:Vben Admin 的组件封装思想
核心实现
1. 类型定义(typing.ts)
首先定义组件所需的类型,确保类型安全:
import type { ReactNode } from 'react';
import type { DescriptionsProps } from 'antd';
// 描述项配置接口
export interface DescItem {
labelMinWidth?: number;
contentMinWidth?: number;
labelStyle?: React.CSSProperties;
field: string;
label: ReactNode;
span?: number;
show?: (data: Record<string, any>) => boolean;
render?: (value: any, data: Record<string, any>) => ReactNode;
}
// 组件属性接口
export interface DescriptionProps extends AntDescriptionsProps {
schema?: DescItem[];
data?: Record<string, any>;
column?: number;
}
// 组件实例接口
export interface DescInstance {
setDescProps: (props: Partial<DescriptionProps>) => void;
}
2. 组件封装(Description.tsx)
使用 forwardRef 转发 ref,结合 useState 管理动态 props,通过 useImperativeHandle 暴露实例方法:
import React, { useRef, useState, useImperativeHandle, forwardRef } from "react";
import { Descriptions } from "antd";
import type { DescriptionProps, DescInstance, DescItem } from "./typing";
const Description = forwardRef<DescInstance, DescriptionProps>((props, ref) => {
const [innerProps, setInnerProps] = useState<DescriptionProps>(props);
const mergedProps = { ...innerProps, ...props };
const {
schema,
data,
column = 2,
bordered = true,
contentStyle,
labelStyle,
size = "default",
...descriptionsProps
} = mergedProps;
useImperativeHandle(
ref,
() => ({
setDescProps: (newProps) => {
setInnerProps((prev) => ({ ...prev, ...newProps }));
},
}),
[]
);
const renderLabel = ({ label, labelMinWidth }: DescItem) => {
if (!labelMinWidth || !labelStyle) return label;
return <div style={{ minWidth: `${labelMinWidth}px`, ...labelStyle }}>{label}</div>;
};
const renderDescriptionItem = (item: DescItem) => {
const { label, field, span, show, render, contentMinWidth } = item;
const value = data ? data[field] : undefined;
if (show && !show(data)) return null;
const getContent = () => render ? render(value, data) : value;
return (
<Descriptions.Item
key={field}
label={renderLabel(item)}
span={span}
styles={{ label: { ...labelStyle }, content: { ...contentStyle } }}
>
{contentMinWidth ? <div style={{ minWidth: `${contentMinWidth}px` }}>{getContent()}</div> : getContent()}
</Descriptions.Item>
);
};
return (
<Descriptions
column={column}
bordered={bordered}
size={size}
{...descriptionsProps}
>
{schema?.map((item) => renderDescriptionItem(item))}
</Descriptions>
);
});
Description.displayName = "Description";
export default Description;
3. 自定义 Hook(useDescription.ts)
封装自定义 Hook 用于管理组件实例和状态更新:
import { useRef, useEffect } from 'react';
import type { MutableRefObject } from 'react';
import type { DescriptionProps, DescInstance } from './typing';
export function useDescription(initialProps?: Partial<DescriptionProps>) {
const descRef = useRef<DescInstance | null>(null);
const isLoaded = useRef(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const register = (instance: DescInstance | null) => {
if (instance) {
descRef.current = instance;
isLoaded.current = true;
if (initialProps) instance.setDescProps(initialProps);
} else {
descRef.current = null;
isLoaded.current = false;
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}
};
const setDescProps = (props: Partial<DescriptionProps>) => {
if (!isLoaded.current) return;
if (descRef.current) {
descRef.current.setDescProps(props);
} else if (!timerRef.current) {
timerRef.current = setTimeout(() => {
setDescProps(props);
timerRef.current = null;
}, 100);
}
};
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
isLoaded.current = false;
descRef.current = null;
};
}, []);
return {
register,
setDescProps,
descRef: descRef as MutableRefObject<DescInstance>
};
}
4. 组件导出(index.ts)
规范组件导出:
import Description from './src/Description.tsx';
export { Description }
功能特性
-
动态配置:通过 schema 定义描述项,支持自定义标签、内容、样式
-
数据驱动:通过 data 属性动态渲染内容
-
条件渲染:支持 show 函数控制描述项显示/隐藏
-
自定义渲染:支持 render 函数自定义内容展示
-
实例方法:通过 useDescription hook 获取实例,调用 setDescProps 动态更新
-
类型安全:完善的 TypeScript 类型定义
使用示例
import { Description } from './components/Descrption';
import { useDescription } from './components/Descrption/src/useDescription';
const App = () => {
const { register } = useDescription({
schema: detailSchema,
data,
title: "基本信息",
column: 2,
labelStyle: { minWidth: "250px" },
contentStyle: { minWidth: "300px" },
});
const schema = [
{ label: '姓名', field: 'name', labelMinWidth: 100 },
{ label: '年龄', field: 'age' },
{ label: '邮箱', field: 'email', render: (value) => <a href={`mailto:${value}`}>{value}</a> },
{ label: '地址', field: 'address', span: 2, show: (data) => data.address },
];
const data = {
name: '张三',
age: 28,
email: 'zhangsan@example.com',
address: '北京市海淀区'
};
return (
<Description
ref={register}
/>
);
};
总结
本文介绍了如何基于 Ant Design 和 React,参考 Vben Admin 的设计思想,封装一个功能完善的描述列表组件。通过 TypeScript 类型定义确保类型安全,使用 React hooks 管理状态和实例,支持动态配置和自定义渲染,满足各种复杂场景的需求。这种组件封装方式不仅提高了代码复用性,也便于维护和扩展,是现代前端开发中的最佳实践之一。