feat(data-table): column.rowSpan & colSpan

This commit is contained in:
07akioni 2021-03-24 17:17:24 +08:00
parent b59309fee8
commit f56d978344
14 changed files with 436 additions and 141 deletions

View File

@ -2,6 +2,10 @@
## Pending
### Feats
- `n-data-table`'s column add `colSpan` and `rowSpan` prop.
### Fixes
- Fix `n-dropdown` with `x` and `y` set logs errors when mouse move outside it.
@ -20,7 +24,7 @@
- `n-popover` default `delay` is set to `100`.
- `n-tooltip` default `showArrow` is set to `true`.
### Feat
### Feats
- `n-config-provider` prop `theme-overrides` support inheritance.
- `n-card` add `hoverable` prop.
@ -39,7 +43,7 @@
## 2.0.1
### Feat
### Feats
- `n-layout-sider` add `default-collapsed` prop.
- `n-modal` support custom position.

View File

@ -2,6 +2,10 @@
## Pending
### Feats
- `n-data-table` column 新增 `colSpan``rowSpan` 属性
### Fixes
- 修正 `n-dropdown` 在设定 `x``y` 之后鼠标在外面移动会报错
@ -20,7 +24,7 @@
- `n-popover` 默认 `delay` 设为 `100`
- `n-tooltip` 默认 `showArrow` 设为 `true`
### Feat
### Feats
- `n-config-provider``theme-overrides` 支持继承
- `n-card` 新增 `hoverable` 属性
@ -39,7 +43,7 @@
## 2.0.1
### Feat
### Feats
- `n-layout-sider` 新增 `default-collapsed` 属性
- `n-modal` 支持自定义位置

View File

@ -5,9 +5,10 @@
```
```js
import { h, resolveComponent } from 'vue'
import { h, defineComponent } from 'vue'
import { NTag, NButton, useMessage } from 'naive-ui'
const createColumns = (instance) => {
const createColumns = ({ sendMail }) => {
return [
{
title: 'Name',
@ -31,14 +32,16 @@ const createColumns = (instance) => {
render (row) {
const tags = row.tags.map((tagKey) => {
return h(
resolveComponent('n-tag'),
NTag,
{
style: {
marginRight: '6px'
},
type: 'info'
},
{ default: () => tagKey }
{
default: () => tagKey
}
)
})
return tags
@ -50,10 +53,10 @@ const createColumns = (instance) => {
width: '20%',
render (row) {
return h(
resolveComponent('n-button'),
NButton,
{
size: 'small',
onClick: () => instance.sendMail(row)
onClick: () => sendMail(row)
},
{ default: () => 'Send Email' }
)
@ -62,7 +65,7 @@ const createColumns = (instance) => {
]
}
const data = [
const createData = () => [
{
key: 0,
name: 'John Brown',
@ -86,23 +89,20 @@ const data = [
}
]
export default {
inject: ['message'],
data () {
export default defineComponent({
setup () {
const message = useMessage()
return {
data: data,
columns: createColumns(this)
}
},
computed: {
pagination () {
return { total: this.data.length, pageSize: 10 }
}
},
methods: {
sendMail (rowData) {
this.message.info('send mail to ' + rowData.name)
data: createData(),
columns: createColumns({
sendMail (rowData) {
message.info('send mail to ' + rowData.name)
}
}),
pagination: {
pageSize: 10
}
}
}
}
})
```

View File

@ -110,7 +110,7 @@ export default {
},
computed: {
pagination () {
return { total: this.data.length, pageSize: 10 }
return { pageSize: 10 }
}
},
methods: {

View File

@ -15,6 +15,7 @@ basic
empty
border
size
merge-cell
filter-and-sorter
select
group-header
@ -79,6 +80,7 @@ These methods can help you control table in an uncontrolled manner. However, it'
| align | `'left' \| 'right' \| 'center'` | `'left'` | Text align in column |
| children | `Column[]` | `undefined` | Child nodes of a grouped column |
| className | `string` | `undefined` | |
| colSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
| defaultFilterOptionValue | `string \| number \| null` | `null` | The default active filter option value in uncontrolled manner. (works when not using multiple filters) |
| defaultFilterOptionValues | `Array<string \| number>` | `[]` | The default active filter option values in uncontrolled manner. (works when there are multiple filters) |
| defaultSortOrder | `'descend' \| 'ascend' \| false` | `false` | The default sort order of the table in uncontrolled manner |
@ -94,6 +96,7 @@ These methods can help you control table in an uncontrolled manner. However, it'
| key | `string \| number` | **required** | Unique key of this column, **required** when table's row-key is not set. |
| render | `(rowData: Object) => VNodeChild` | `undefined` | Render function of column row cell. |
| renderFilterMenu | `() => VNodeChild` | `undefined` | Render function of column filter menu. |
| rowSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
| sortOrder | `'descend' \| 'ascend' \| false` | `undefined` | The controlled sort order of the column. If multiple columns' sortOrder is set, the first one will affect. |
| sorter | `boolean \| function \| 'default'` | `false` | The sorter of the column. If set `'default'`, it will use a basic builtin compare function. If set to `true`, it will only display sort icon on the column, which can be used in async status. Otherwise it works like `Array.sort`'s compare function. |
| title | `string \| (() => VNodeChild)` | `undefined` | Can be a render function |

View File

@ -0,0 +1,118 @@
# Merge Cell
Set colspan and rowspan by setting `colSpan` and `rowSpan` of column object.
```html
<n-data-table
:columns="columns"
:data="data"
:pagination="pagination"
:single-line="false"
/>
```
```js
import { h, defineComponent } from 'vue'
import { NTag, NButton, useMessage } from 'naive-ui'
const createColumns = ({ sendMail }) => {
return [
{
title: 'Name',
key: 'name',
width: '15%',
rowSpan: (rowData, rowIndex) => (rowIndex === 0 ? 2 : 1),
colSpan: (rowData, rowIndex) => (rowIndex === 0 ? 2 : 1)
},
{
title: 'Age',
key: 'age',
width: '10%'
},
{
title: 'Address',
key: 'address',
width: '20%',
colSpan: (rowData, rowIndex) => (rowIndex === 2 ? 2 : 1)
},
{
title: 'Tags',
key: 'tags',
width: '20%',
render (row) {
const tags = row.tags.map((tagKey) => {
return h(
NTag,
{
style: {
marginRight: '6px'
},
type: 'info'
},
{
default: () => tagKey
}
)
})
return tags
}
},
{
title: 'Action',
key: 'actions',
width: '20%',
render (row) {
return h(
NButton,
{
size: 'small',
onClick: () => sendMail(row)
},
{ default: () => 'Send Email' }
)
}
}
]
}
const createData = () => [
{
key: 0,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer']
},
{
key: 1,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser']
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher']
}
]
export default defineComponent({
setup () {
const message = useMessage()
return {
data: createData(),
columns: createColumns({
sendMail (rowData) {
message.info('send mail to ' + rowData.name)
}
}),
pagination: {
pageSize: 10
}
}
}
})
```

View File

@ -109,7 +109,7 @@ export default {
},
computed: {
pagination () {
return { total: this.data.length, pageSize: 10 }
return { pageSize: 10 }
}
},
methods: {

View File

@ -5,9 +5,10 @@
```
```js
import { h, resolveComponent } from 'vue'
import { h, defineComponent } from 'vue'
import { NTag, NButton, useMessage } from 'naive-ui'
const createColumns = (instance) => {
const createColumns = ({ sendMail }) => {
return [
{
title: 'Name',
@ -31,7 +32,7 @@ const createColumns = (instance) => {
render (row) {
const tags = row.tags.map((tagKey) => {
return h(
resolveComponent('n-tag'),
NTag,
{
style: {
marginRight: '6px'
@ -52,10 +53,10 @@ const createColumns = (instance) => {
width: '20%',
render (row) {
return h(
resolveComponent('n-button'),
NButton,
{
size: 'small',
onClick: () => instance.sendMail(row)
onClick: () => sendMail(row)
},
{ default: () => 'Send Email' }
)
@ -64,7 +65,7 @@ const createColumns = (instance) => {
]
}
const data = [
const createData = () => [
{
key: 0,
name: 'John Brown',
@ -88,23 +89,20 @@ const data = [
}
]
export default {
inject: ['message'],
data () {
export default defineComponent({
setup () {
const message = useMessage()
return {
data: data,
columns: createColumns(this)
}
},
computed: {
pagination () {
return { total: this.data.length, pageSize: 10 }
}
},
methods: {
sendMail (rowData) {
this.message.info('send mail to ' + rowData.name)
data: createData(),
columns: createColumns({
sendMail (rowData) {
message.info('send mail to ' + rowData.name)
}
}),
pagination: {
pageSize: 10
}
}
}
}
})
```

View File

@ -110,7 +110,7 @@ export default {
},
computed: {
pagination () {
return { total: this.data.length, pageSize: 10 }
return { pageSize: 10 }
}
},
methods: {

View File

@ -15,6 +15,7 @@ basic
empty
border
size
merge-cell
filter-and-sorter
select
group-header
@ -79,6 +80,7 @@ custom-filter-menu
| align | `'left' \| 'right' \| 'center'` | `'left'` | 列内的文本排列 |
| children | `Column[]` | `undefined` | 成组列头的子节点 |
| className | `string` | `undefined` | |
| colSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
| defaultFilterOptionValue | `string \| number \| null` | `null` | 非受控状态下默认的过滤器选项值(过滤器单选时生效) |
| defaultFilterOptionValues | `Array<string \| number>` | `[]` | 非受控状态下默认的过滤器选项值(过滤器多选时生效) |
| defaultSortOrder | `'descend' \| 'ascend' \| false` | `false` | 非受控状态下表格默认的排序方式 |
@ -94,6 +96,7 @@ custom-filter-menu
| key | `string \| number` | **必须** | 这一列的 key在表格未设定 row-key 的时候是**必须**的。 |
| render | `(rowData: Object) => VNodeChild` | `undefined` | 渲染函数,渲染这一列的每一行的单元格 |
| renderFilterMenu | `() => VNodeChild` | `undefined` | 渲染函数,渲染这一列的过滤器菜单 |
| rowSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
| sortOrder | `'descend' \| 'ascend' \| false` | `undefined` | 受控状态下表格的排序方式。如果多列都设定了有效值,那么只有第一个会生效 |
| sorter | `boolean \| function \| 'default'` | `undefined` | 这一列的排序方法。如果设为 `'default'` 表格将会使用一个内置的排序函数;如果设为 `true`,表格将只会在这列展示一个排序图标,在异步的时候可能有用。其他情况下它工作的方式类似 `Array.sort` 的对比函数 |
| title | `string \| (() => VNodeChild)` | `undefined` | 可以是渲染函数 |

View File

@ -0,0 +1,118 @@
# 合并单元格
设定列的 `colSpan``rowSpan` 来控制单元格的 `colspan``rowspan`
```html
<n-data-table
:columns="columns"
:data="data"
:pagination="pagination"
:single-line="false"
/>
```
```js
import { h, defineComponent } from 'vue'
import { NTag, NButton, useMessage } from 'naive-ui'
const createColumns = ({ sendMail }) => {
return [
{
title: 'Name',
key: 'name',
width: '15%',
rowSpan: (rowData, rowIndex) => (rowIndex === 0 ? 2 : 1),
colSpan: (rowData, rowIndex) => (rowIndex === 0 ? 2 : 1)
},
{
title: 'Age',
key: 'age',
width: '10%'
},
{
title: 'Address',
key: 'address',
width: '20%',
colSpan: (rowData, rowIndex) => (rowIndex === 2 ? 2 : 1)
},
{
title: 'Tags',
key: 'tags',
width: '20%',
render (row) {
const tags = row.tags.map((tagKey) => {
return h(
NTag,
{
style: {
marginRight: '6px'
},
type: 'info'
},
{
default: () => tagKey
}
)
})
return tags
}
},
{
title: 'Action',
key: 'actions',
width: '20%',
render (row) {
return h(
NButton,
{
size: 'small',
onClick: () => sendMail(row)
},
{ default: () => 'Send Email' }
)
}
}
]
}
const createData = () => [
{
key: 0,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer']
},
{
key: 1,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser']
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher']
}
]
export default defineComponent({
setup () {
const message = useMessage()
return {
data: createData(),
columns: createColumns({
sendMail (rowData) {
message.info('send mail to ' + rowData.name)
}
}),
pagination: {
pageSize: 10
}
}
}
})
```

View File

@ -109,7 +109,7 @@ export default {
},
computed: {
pagination () {
return { total: this.data.length, pageSize: 10 }
return { pageSize: 10 }
}
},
methods: {

View File

@ -64,95 +64,138 @@ export default defineComponent({
onScroll={handleScroll}
>
{{
default: () => (
<table ref="body" class="n-data-table-table">
<colgroup>
{NDataTable.cols.map((col) => (
<col key={col.key} style={col.style}></col>
))}
</colgroup>
<tbody ref="tbody" class="n-data-table-tbody">
{NDataTable.paginatedData.map((tmNode, index) => {
const { rawNode: row } = tmNode
const { handleCheckboxUpdateChecked } = this
const {
mergedTheme,
cols,
fixedColumnLeftMap,
fixedColumnRightMap,
currentPage,
mergedCheckedRowKeys,
rowClassName,
leftActiveFixedColKey,
rightActiveFixedColKey
} = NDataTable
return (
<tr
key={tmNode.key}
class={[
'n-data-table-tr',
createRowClassName(row, index, rowClassName)
]}
>
{cols.map((col) => {
const { key, column } = col
return (
<td
key={key}
style={{
textAlign: column.align || undefined,
left: pxfy(fixedColumnLeftMap[key]),
right: pxfy(fixedColumnRightMap[key])
}}
class={[
'n-data-table-td',
column.className,
column.fixed &&
`n-data-table-td--fixed-${column.fixed}`,
column.align &&
`n-data-table-td--${column.align}-align`,
{
'n-data-table-td--ellipsis':
column.ellipsis === true ||
// don't add ellpisis class if tooltip exists
(column.ellipsis && !column.ellipsis.tooltip),
'n-data-table-td--shadow-after':
leftActiveFixedColKey === key,
'n-data-table-td--shadow-before':
rightActiveFixedColKey === key,
'n-data-table-td--selection':
column.type === 'selection'
}
]}
>
{column.type === 'selection' ? (
<NCheckbox
key={currentPage}
disabled={column.disabled?.(row)}
checked={mergedCheckedRowKeys.includes(
tmNode.key
)}
onUpdateChecked={(checked) =>
handleCheckboxUpdateChecked(tmNode, checked)
default: () => {
const cordToPass: Record<number, number[]> = {}
return (
<table ref="body" class="n-data-table-table">
<colgroup>
{NDataTable.cols.map((col) => (
<col key={col.key} style={col.style}></col>
))}
</colgroup>
<tbody ref="tbody" class="n-data-table-tbody">
{NDataTable.paginatedData.map((tmNode, rowIndex) => {
const { rawNode: row } = tmNode
const { handleCheckboxUpdateChecked } = this
const {
mergedTheme,
cols,
fixedColumnLeftMap,
fixedColumnRightMap,
currentPage,
mergedCheckedRowKeys,
rowClassName,
leftActiveFixedColKey,
rightActiveFixedColKey
} = NDataTable
return (
<tr
key={tmNode.key}
class={[
'n-data-table-tr',
createRowClassName(row, rowIndex, rowClassName)
]}
>
{cols.map((col, colIndex) => {
if (rowIndex in cordToPass) {
const cordOfRowToPass = cordToPass[rowIndex]
const indexInCordOfRowToPass = cordOfRowToPass.indexOf(
colIndex
)
if (~indexInCordOfRowToPass) {
cordOfRowToPass.splice(indexInCordOfRowToPass, 1)
return null
}
}
const { key, column } = col
const { rowSpan, colSpan } = column
const mergedColSpan = colSpan
? colSpan(row, rowIndex)
: 1
const mergedRowSpan = rowSpan
? rowSpan(row, rowIndex)
: 1
if (mergedColSpan > 1 || mergedRowSpan > 1) {
for (
let i = rowIndex;
i < rowIndex + mergedRowSpan;
++i
) {
for (
let j = colIndex;
j < colIndex + mergedColSpan;
++j
) {
if (i === rowIndex && j === colIndex) continue
if (!(i in cordToPass)) {
cordToPass[i] = [j]
} else {
cordToPass[i].push(j)
}
/>
) : (
<Cell
index={index}
row={row}
column={column}
mergedTheme={mergedTheme}
/>
)}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
)
}
}
}
return (
<td
key={key}
style={{
textAlign: column.align || undefined,
left: pxfy(fixedColumnLeftMap[key]),
right: pxfy(fixedColumnRightMap[key])
}}
colspan={mergedColSpan}
rowspan={mergedRowSpan}
class={[
'n-data-table-td',
column.className,
column.fixed &&
`n-data-table-td--fixed-${column.fixed}`,
column.align &&
`n-data-table-td--${column.align}-align`,
{
'n-data-table-td--ellipsis':
column.ellipsis === true ||
// don't add ellpisis class if tooltip exists
(column.ellipsis &&
!column.ellipsis.tooltip),
'n-data-table-td--shadow-after':
leftActiveFixedColKey === key,
'n-data-table-td--shadow-before':
rightActiveFixedColKey === key,
'n-data-table-td--selection':
column.type === 'selection'
}
]}
>
{column.type === 'selection' ? (
<NCheckbox
key={currentPage}
disabled={column.disabled?.(row)}
checked={mergedCheckedRowKeys.includes(
tmNode.key
)}
onUpdateChecked={(checked) =>
handleCheckboxUpdateChecked(tmNode, checked)
}
/>
) : (
<Cell
index={rowIndex}
row={row}
column={column}
mergedTheme={mergedTheme}
/>
)}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
)
}
}}
</NScrollbar>
)

View File

@ -84,6 +84,8 @@ export type TableColumnInfo = {
renderFilterMenu?: FilterMenuRender
renderSorter?: SorterRender
renderFilter?: FilterRender
colSpan?: (data: TableNode, index: number) => number
rowSpan?: (data: TableNode, index: number) => number
} & CommonColInfo
export type SelectionColInfo = {
@ -96,6 +98,8 @@ export type SelectionColInfo = {
filterOptions?: never
filterOptionValues?: never
filterOptionValue?: never
colSpan?: never
rowSpan?: never
} & CommonColInfo
export type TableColumn = TableColumnGroup | TableColumnInfo | SelectionColInfo