一、组件介绍
官网链接:Descriptions 组件 | Element (gitee.io)。
el-descriptions组件是一个展示类组件,其使用表格展示多个字段的信息;需要和el-descriptions-item子组件搭配使用。
1.1 descriptions 属性
- border: boolean类型,是否展示边框;
- direction:string类型,label与content的排列方向,即label是在左方或上方,可选值:
horizontal/vertical,默认值:horizontal; - size: string类型,列表的尺寸,可选值
medium/small/mini,默认值:medium; - column: number类型,一行中展示多少个单位的
descriptions-item,默认值:3; - title:string类型,标题,展示在左上方;
- extra: string类型,操作区文本,展示在右上方;
1.2 descriptions-item 属性
- label: string类型,标签文本;
- span: number类型,此列所占的宽度单位数量,与
descriptions的column相关; - width: number/string类型,宽度,不同行相同列的按最大值设定;
- min-width: number/string类型,列最小宽度,
min-width会把剩余宽度按比例分配给设置了min-width的列; - align/label-align: string类型,列内容/标签的对齐方式,可选值:
left / center / right,默认值:left; - class-name/label-class-name:: string类型,列内容/标签的自定义class名称;
二、源码分析
2.1 descriptions源码
2.1.1 template部分
<template>
<div class="el-descriptions">
<!-- 头部,左侧展示标题,右侧展示操作区 -->
<div v-if="title || extra || $slots.title || $slots.extra" class="el-descriptions__header">
<!-- 标题 -->
<div class="el-descriptions__title">
<slot name="title">{{ title }}</slot>
</div>
<!-- 操作区 -->
<div class="el-descriptions__extra">
<slot name="extra">{{ extra }}</slot>
</div>
</div>
<div class="el-descriptions__body">
<!-- 使用table进行展示 -->
<table
:class="['el-descriptions__table', { 'is-bordered': border }, descriptionsSize ? `el-descriptions--${descriptionsSize}` : '']"
>
<!-- 没有thead -->
<tbody>
<!-- 循环rows -->
<template v-for="(row, index) in getRows()" :key="index">
<el-descriptions-row :row="row" />
</template>
</tbody>
</table>
</div>
</div>
</template>
2.1.2 script部分
setup(props, { slots }) {
// 向子组件提供数据
provide(elDescriptionsKey, props)
const $ELEMENT = useGlobalConfig()
// 组件的size
const descriptionsSize = computed(() => {
return props.size || $ELEMENT.size
})
// 数据扁平化方法:使用递归,使嵌套的数据变成一维数组
const flattedChildren = children => {
const temp = Array.isArray(children) ? children : [children]
const res = []
temp.forEach(child => {
if (Array.isArray(child.children)) {
res.push(...flattedChildren(child.children))
} else {
res.push(child)
}
})
return res
}
// 主要是根据当前行的剩余宽度单位设置这一列的宽度单位
const filledNode = (node, span, count, isLast = false) => {
if (!node.props) {
// 避免node.props是undefined,方便后续设置span
node.props = {}
}
if (span > count) {
// 如果当前列的span设置超过了剩余的宽度单位,则设置成剩余的宽度单位
node.props.span = count
}
if (isLast) {
// 如果是最后一列的元素,设置成其本身的宽度单位
node.props.span = span
}
return node
}
const getRows = () => {
// 将默认插槽中的ElDescriptionsItem元素进行扁平化处理,得到一维数组
// 一维数组中的每个元素就是一列
const children = flattedChildren(slots.default?.()).filter(node => node?.type?.name === 'ElDescriptionsItem')
// 行数据
const rows = []
// 临时数组,用于存储每一行的列
let temp = []
// count是指当前行剩余可分配的宽度单位
let count = props.column
// 累计每一列所占的宽度单位之和
let totalSpan = 0
// 循环处理每一列
children.forEach((node, index) => {
// 当前列所占宽度单位,不传入的话,默认是1
let span = node.props?.span || 1
// 不是最后一个列元素时
if (index < children.length - 1) {
// 累积每一列所占的宽度单位
totalSpan += (span > count ? count : span)
}
// 最后一个列元素
if (index === children.length - 1) {
// 计算最后一个元素可占的剩余宽度单位
const lastSpan = props.column - totalSpan % props.column
// 将最后一个列元素push到临时row中
temp.push(filledNode(node, lastSpan, count, true))
// 最后一个列元素也放到临时row了,将临时row push到rows中
rows.push(temp)
return
}
// 非最后一个列元素
// 如果列宽度小于当前行剩余宽度,就放到当前临时行
if (span < count) {
// 当前行剩余宽度单位减去当前列元素
count -= span
// 当前列元素放入到当前临时行
temp.push(node)
} else {
// 如果列元素宽度大于等于当前行剩余宽度
// 将列元素放入到当前行,使用filledNode将宽度设置成当前行剩余的宽度
temp.push(filledNode(node, span, count))
// 当前列元素放入到当前临时行
rows.push(temp)
// 重置状态,开始新的一行
// 恢复count
count = props.column
// 清空当前临时行
temp = []
}
})
return rows
}
return {
descriptionsSize,
getRows,
}
}
2.1.3 总结
- header部分使用插槽,提供
title/extra2个插槽; - 使用table展示数据,没有使用thead,使用tbody;
- 根据columns属性和默认插槽中的
descriotions-item元素的span属性,逐行生成每一行;
2.2 descriptions-row 源码
2.2.1 script部分
<template>
<!-- 垂直方向布局 -->
<template v-if="descriptions.direction === 'vertical'">
<!-- 每一个row,渲染2个tr,分别是label和content -->
<tr>
<!-- lable行 -->
<template v-for="(cell, index) in row" :key="`tr1-${index}`">
<el-descriptions-cell :cell="cell" tag="th" type="label" />
</template>
</tr>
<tr>
<!-- 内容行 -->
<template v-for="(cell, index) in row" :key="`tr2-${index}`">
<el-descriptions-cell :cell="cell" tag="td" type="content" />
</template>
</tr>
</template>
<!-- 水平方向布局 -->
<!-- 每一个row,渲染1个tr -->
<tr v-else>
<template v-for="(cell, index) in row" :key="`tr3-${index}`">
<!-- 有边框情况 -->
<template v-if="descriptions.border">
<el-descriptions-cell :cell="cell" tag="td" type="label" />
<el-descriptions-cell :cell="cell" tag="td" type="content" />
</template>
<!-- 无边框情况 -->
<el-descriptions-cell
v-else
:cell="cell"
tag="td"
type="both"
/>
</template>
</tr>
</template>
2.2.2 script部分
setup() {
// 注入 descriptions组件提供的数据
const descriptions = inject(elDescriptionsKey, {} as IDescriptionsInject)
return {
descriptions,
}
},
2.3 description-cell 源码
export default defineComponent({
name: "ElDescriptionsCell",
props: {
cell: {
type: Object,
},
tag: {
type: String,
},
type: {
type: String,
},
},
setup() {
// 注入descriptions组件提供的数据
const descriptions = inject(elDescriptionsKey, {} as IDescriptionsInject);
return {
descriptions,
};
},
render() {
// props属性格式化
const item = getNormalizedProps(this.cell as VNode) as IDescriptionsItemInject;
// descriptions-item的label插槽优先于label属性
const label = this.cell?.children?.label?.() || item.label;
// content内容是descriptions-item的默认插槽
const content = this.cell?.children?.default?.();
const span = item.span;
const align = item.align ? `is-${item.align}` : "";
const labelAlign = item.labelAlign ? `is-${item.labelAlign}` : "" || align;
const className = item.className;
const labelClassName = item.labelClassName;
const style = {
width: addUnit(item.width),
minWidth: addUnit(item.minWidth),
};
// 根据type,渲染不同的内容
switch (this.type) {
case "label":
// 渲染label
return h(
this.tag,
{
style: style,
class: [
"el-descriptions__cell",
"el-descriptions__label",
{ "is-bordered-label": this.descriptions.border },
labelAlign,
labelClassName,
],
colSpan: this.descriptions.direction === "vertical" ? span : 1,
},
label
);
case "content":
// 渲染content
return h(
this.tag,
{
style: style,
class: ["el-descriptions__cell", "el-descriptions__content", align, className],
colSpan: this.descriptions.direction === "vertical" ? span : span * 2 - 1,
},
content
);
default:
// type是both时;一个td中同时包含label和content
return h(
"td",
{
style: style,
class: [align],
colSpan: span,
},
[
h(
"span",
{
class: ["el-descriptions__cell", "el-descriptions__label", labelClassName],
},
label
),
h(
"span",
{
class: ["el-descriptions__cell", "el-descriptions__content", className],
},
content
),
]
);
}
},
});