diff --git a/packages/ali-react-table/src/base-table/html-table.tsx b/packages/ali-react-table/src/base-table/html-table.tsx
index 816aeb25a..86c15f461 100644
--- a/packages/ali-react-table/src/base-table/html-table.tsx
+++ b/packages/ali-react-table/src/base-table/html-table.tsx
@@ -71,10 +71,12 @@ export function HtmlTable({
rowProps?.className,
)
+ const key = internals.safeGetRowKey(primaryKey, row, rowIndex)
const trProps = {
...rowProps,
className: rowClass,
'data-rowindex': rowIndex,
+ id: `art-table-row-${key}`, // 添加id,可用于滚动精准定位或与其他地方
children: hozInfo.visible.map((descriptor) => {
if (descriptor.type === 'blank') {
return
|
@@ -83,7 +85,6 @@ export function HtmlTable({
}),
}
- const key = internals.safeGetRowKey(primaryKey, row, rowIndex)
if (Row != null && tbodyHtmlTag === 'tbody') {
return React.createElement(Row, { key, row, rowIndex, trProps })
} else {
diff --git a/packages/ali-react-table/src/base-table/table.tsx b/packages/ali-react-table/src/base-table/table.tsx
index c0d000347..86597d449 100644
--- a/packages/ali-react-table/src/base-table/table.tsx
+++ b/packages/ali-react-table/src/base-table/table.tsx
@@ -192,6 +192,127 @@ export class BaseTable extends React.Component {
}
}
+ /**
+ * 定位滚动条
+ * @desc 优先级按参数顺序降序
+ * @param top 目标值
+ * @param element 目标元素
+ * @param rowKey 行主键
+ * @param rowIndex 行索引
+ * @param offset 纵向滚动偏移量,用于抵消固定表头
+ */
+ scrollTo ({ top, element, rowKey, rowIndex, offset }
+ : { element: HTMLElement, top: number, rowKey: string, rowIndex: number, offset: number }
+ ) {
+ if (top > -1) return this.scrollToValue(top)
+ if (element) return this.scrollToElement(element, offset)
+ if (rowKey) return this.scrollToRowKey(rowKey, offset)
+ if (rowIndex) return this.scrollToRowIndex(rowIndex, offset)
+ }
+
+ /**
+ * 定位到指定位置
+ * @desc 动态行高时如需精准定位,可以传入目标元素进行二次修正
+ * @param top 纵向滚动条位置
+ * @param selector 目标元素选择器
+ * @param offset 纵向滚动偏移量,用于抵消固定表头
+ */
+ private scrollToValue (top: number, selector?: string, offset?: number) {
+ let view = this.artTableWrapperRef.current
+ if (view.clientHeight === view.scrollHeight) {
+ view = document.documentElement as HTMLDivElement
+ }
+ if (!selector) {
+ return view.scrollTop = top
+ }
+ const element = view.querySelector(selector)
+ // 目标元素真实存在时直接精准定位
+ if (element) {
+ return this.scrollToElement(element, offset)
+ }
+ view.scrollTop = top
+ // 延迟修正
+ setTimeout(() => {
+ this.scrollToElement(view.querySelector(selector), offset)
+ }, 16)
+ }
+
+ /**
+ * 定位到指定元素
+ * @desc 虚拟表格不应该直接使用该方法
+ * @param element
+ * @param offset 纵向滚动偏移量,用于抵消固定表头
+ */
+ private scrollToElement (element: Element, offset?: number) {
+ if (!element) return
+ let view = this.artTableWrapperRef.current
+ if (view.clientHeight === view.scrollHeight) {
+ view = document.documentElement as HTMLDivElement
+ }
+ view.scrollTop = this.getScrollOffsetTop(element, view) + (offset || 0);
+ }
+
+ /**
+ * 定位到指定行
+ * @desc 树形表格有节点展开时不应该使用此方法
+ * @param rowIndex 行号(从1开始)
+ * @param offset 纵向滚动偏移量,用于抵消固定表头
+ */
+ private scrollToRowIndex (rowIndex: number, offset?: number) {
+ const { cache } = this.rowHeightManager
+ const end = Math.min(rowIndex + 1, cache.length)
+ let scrollTop = 0
+ for (let i = 0; i < end; i++) {
+ scrollTop += cache[i]
+ }
+ this.scrollToValue(scrollTop, `[data-rowindex="${rowIndex}"]`, offset)
+ }
+
+ /**
+ * 根据primaryKey定位到指定行
+ * @desc 支持在树型表格中定位到未展开的节点,调用时应提前更新该节点的所有父节点key至openKeys中,并延迟调用该方法)
+ * @param rowKey 行主键
+ * @param offset 纵向滚动偏移量,用于抵消固定表头
+ */
+ private scrollToRowKey (rowKey: string, offset?: number) {
+ const { dataSource } = this.props
+ const { cache } = this.rowHeightManager
+ const length = dataSource.length
+ const { primaryKey } = this.props
+ let scrollTop = 0
+ for (let i = 1; i < length; i++) {
+ scrollTop += cache[i]
+ if (dataSource[i][primaryKey as string] === rowKey) break
+ }
+ this.scrollToValue(scrollTop, `#art-table-row-${rowKey}`, offset)
+ }
+
+ /**
+ * 获取元素在滚动区域中的位置
+ * @desc 节选自:https://github.com/Stanko/animated-scroll-to
+ * @param element 目标元素
+ * @param view 滚动区域元素
+ * @private
+ */
+ private getScrollOffsetTop (element: Element, view: Element): number {
+ return this.getElementOffset(element).top - this.getElementOffset(view).top;
+ }
+
+ private getScrollOffsetLeft (element: Element, view: Element): number {
+ return this.getElementOffset(element).left - this.getElementOffset(view).left;
+ }
+
+ private getElementOffset (element: any) {
+ let top = 0;
+ let left = 0;
+ do {
+ top += element.offsetTop || 0;
+ left += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return { top, left };
+ }
+
/** 自定义滚动条宽度为table宽度,使滚动条滑块宽度相同 */
private updateStickyScroll() {
const { stickyScroll, artTable, stickyScrollItem } = this.domHelper
diff --git a/packages/ali-react-table/src/pipeline/features/treeMode.tsx b/packages/ali-react-table/src/pipeline/features/treeMode.tsx
index 571e18214..36323089c 100644
--- a/packages/ali-react-table/src/pipeline/features/treeMode.tsx
+++ b/packages/ali-react-table/src/pipeline/features/treeMode.tsx
@@ -38,6 +38,9 @@ export interface TreeModeFeatureOptions {
/** 指定表格每一行元信息的记录字段 */
treeMetaKey?: string | symbol
+
+ /** 指定树节点所在列的code,匹配不到时默认第一列 */
+ treeColumnCode?: string
}
export function treeMode(opts: TreeModeFeatureOptions = {}) {
@@ -79,6 +82,7 @@ export function treeMode(opts: TreeModeFeatureOptions = {}) {
const iconIndent = opts.iconIndent ?? ctx.indents.iconIndent
const iconGap = opts.iconGap ?? ctx.indents.iconGap
const indentSize = opts.indentSize ?? ctx.indents.indentSize
+ const treeColumnCode = opts.treeColumnCode
return pipeline.mapDataSource(processDataSource).mapColumns(processColumns)
@@ -110,7 +114,10 @@ export function treeMode(opts: TreeModeFeatureOptions = {}) {
if (columns.length === 0) {
return columns
}
- const [firstCol, ...others] = columns
+
+ columns = columns.slice()
+ const treeColIndex: number = !treeColumnCode ? 0 : Math.max(columns.findIndex(col => col.code === treeColumnCode), 0)
+ const firstCol: ArtColumn = columns[treeColIndex]
const render = (value: any, record: any, recordIndex: number) => {
const content = internals.safeRender(firstCol, record, recordIndex)
@@ -184,17 +191,16 @@ export function treeMode(opts: TreeModeFeatureOptions = {}) {
})
}
- return [
- {
- ...firstCol,
- title: (
- {internals.safeRenderHeader(firstCol)}
- ),
- render,
- getCellProps: clickArea === 'cell' ? getCellProps : firstCol.getCellProps,
- },
- ...others,
- ]
+ columns[treeColIndex] = {
+ ...firstCol,
+ title: (
+ {internals.safeRenderHeader(firstCol)}
+ ),
+ render,
+ getCellProps: clickArea === 'cell' ? getCellProps : firstCol.getCellProps,
+ }
+
+ return columns
}
}
}
diff --git a/packages/website/docs/pipeline/features/tree-mode.mdx b/packages/website/docs/pipeline/features/tree-mode.mdx
index 5fe506950..20f75c912 100644
--- a/packages/website/docs/pipeline/features/tree-mode.mdx
+++ b/packages/website/docs/pipeline/features/tree-mode.mdx
@@ -65,6 +65,9 @@ export interface TreeModeFeatureOptions {
/** 指定表格每一行元信息的记录字段 */
treeMetaKey?: string | symbol
+
+ /** 指定树节点所在列的code,匹配不到时默认第一列 */
+ treeColumnCode?: string
}
```
diff --git a/packages/website/docs/table/api.mdx b/packages/website/docs/table/api.mdx
index d20a56312..c27b2f17c 100644
--- a/packages/website/docs/table/api.mdx
+++ b/packages/website/docs/table/api.mdx
@@ -116,6 +116,27 @@ function Cell({ row, rowIndex, column, colIndex, tdProps }) {
| `headerCellProps` | `React.ThHTMLAttributes` | 表头单元格的 props |
| `features` | `{ [key: string]: any }` | 功能开关标记,用于对表格功能进行拓展 |
+## 滚动到指定位置
+
+定位到行,支持普通、虚拟表格(固定、动态行高均可):
+```js
+tableRef.current.scrollTo({
+ top: 'number',
+ element: 'HTMLElement',
+ rowKey: 'string',
+ rowIndex: 'number',
+ offset: 'number'
+})
+````
+
+| 字段 | 类型 | |
+| ----------------- | ---------------------------------------------------------- |------------------------------------------------------------------------------------|
+| `top` | `number` | 通过具体数值定位 |
+| `element` | `HTMLElement` | 通过目标元素定位 |
+| `rowKey` | `string` | 通过行数据的主键值定位,可用于树形虚拟表格中任意节点的定位。如需定位未展开的节点,可先根据此rowKey递归展开父节点,[详见示例](/examples/scroll-to)|
+| `rowIndex` | `number` | 通过行索引定位 |
+| `offset` | `number` | 纵向滚动偏移量,可用于抵消固定表头 |
+
## CSS 变量列表
`` 中大部分样式都是通过 CSS variables 来定义的,你可以通过下面的方式对表格进行风格化:
diff --git a/packages/website/docs/table/examples.mdx b/packages/website/docs/table/examples.mdx
index 140558293..6b169add3 100644
--- a/packages/website/docs/table/examples.mdx
+++ b/packages/website/docs/table/examples.mdx
@@ -343,3 +343,8 @@ function 表格页脚() {
)
}
```
+
+
+### 滚动到指定位置
+
+[完整示例](/examples/scroll-to)
diff --git a/packages/website/example-sidebars.js b/packages/website/example-sidebars.js
index 76c666aa7..820137b7d 100644
--- a/packages/website/example-sidebars.js
+++ b/packages/website/example-sidebars.js
@@ -4,6 +4,7 @@ module.exports = {
'table',
'pipeline',
'big-data',
+ 'scroll-to',
'theme',
{
type: 'category',
diff --git a/packages/website/examples/pipeline.stories.tsx b/packages/website/examples/pipeline.stories.tsx
index abf5f15a8..b9316f29c 100644
--- a/packages/website/examples/pipeline.stories.tsx
+++ b/packages/website/examples/pipeline.stories.tsx
@@ -1,5 +1,5 @@
import * as fusion from '@alifd/next'
-import { Button } from '@alifd/next'
+import { Button, Divider, Radio } from '@alifd/next'
import { ArtColumn, collectNodes, features, isLeafNode, useTablePipeline } from 'ali-react-table'
import {
columns1,
@@ -26,10 +26,11 @@ export default { title: 'pipeline 功能拓展' }
export function 树形表格() {
const [openKeys, onChangeOpenKeys] = useState(['4', '4-2'])
+ const [treeColumnCode, setTreeColumnCode] = useState('title')
const pipeline = useTablePipeline({ components: fusion as any })
.input({ dataSource: dataSource4, columns: columns4 })
.primaryKey('id')
- .use(features.treeMode({ openKeys, onChangeOpenKeys }))
+ .use(features.treeMode({ openKeys, onChangeOpenKeys, treeColumnCode }))
const allParentKeys = collectNodes(dataSource4, 'pre')
.filter((row) => !isLeafNode(row))
@@ -41,6 +42,14 @@ export function 树形表格() {
+
+ 树节点所在列:
+ setTreeColumnCode(v)}
+ dataSource={columns4.slice(0, 4).map(({ code, name }: any) => ({ label: name, value: code }))}
+ />
openKeys: {openKeys.join(', ')} {openKeys.length === 0 && '[空]'}
diff --git a/packages/website/examples/scroll-to.mdx b/packages/website/examples/scroll-to.mdx
new file mode 100644
index 000000000..9f3393317
--- /dev/null
+++ b/packages/website/examples/scroll-to.mdx
@@ -0,0 +1,11 @@
+---
+id: scroll-to
+title: 滚动到指定位置
+hide_table_of_contents: true
+hide_title: true
+---
+
+import * as stories from './scroll-to.stories.tsx'
+import { Stories } from './helpers/Stories'
+
+
diff --git a/packages/website/examples/scroll-to.stories.tsx b/packages/website/examples/scroll-to.stories.tsx
new file mode 100644
index 000000000..519203e2e
--- /dev/null
+++ b/packages/website/examples/scroll-to.stories.tsx
@@ -0,0 +1,143 @@
+import { ArtColumn, features, useTablePipeline } from 'ali-react-table'
+import { WebsiteBaseTable } from 'assets/WebsiteBaseTable'
+import React, { useRef, useState } from 'react'
+import { Button, Checkbox, Divider } from "@alifd/next";
+
+export default { title: '滚动到指定位置' }
+
+interface Node {
+ id: string,
+ pid: string,
+ name?: string,
+ value?: string,
+ children?: Node[]
+}
+
+const ROOT_ID = '0'
+const NODE = 10000 // 一级节点数量
+const CHILDREN = 3 // 后代节点数量
+const TEXT = 100 // 随机文本长度
+
+const random = (count: number) => Math.ceil(Math.random() * count)
+
+const buildData = function (depth: number, size: number, pid: string, parents: Node[]) {
+ for (let i = 0; i < size; i++) {
+ const id = [pid, i + 1].join('-')
+ const node: Node = {
+ id,
+ pid: pid,
+ name: `键-${id}`,
+ value: new Array(random(TEXT)).fill('文本').join('-'),
+ children: []
+ }
+ parents.push(node)
+ depth > 0 && buildData(depth - 1, CHILDREN, node.id, node.children)
+ }
+}
+
+const dataSource: Node[] = []
+buildData(3, NODE, ROOT_ID, dataSource)
+
+/**
+ * 根据节点查找所有上级节点,返回完整层级的key集合
+ * @param node
+ * @param tree
+ * @param idProp
+ * @param pidProp
+ */
+function findParents (node: any, tree: any[], idProp = 'id', pidProp = 'pid'): string[] {
+ const findParentById = (id: string, nodes: any[], parents: any[]) => {
+ for (let i = 0; i < nodes.length; i++) {
+ let item = nodes[i]
+ if (item[idProp] === id) {
+ return parents.push(item)
+ }
+ if (item.children && item.children.length) {
+ findParentById(id, item.children, parents)
+ }
+ }
+ }
+ const findParentChainById = (id: string, nodes: any[], parents: any[] = [], index = 0) => {
+ if (id === '0') return
+ findParentById(id, nodes, parents)
+ findParentChainById(parents[index][pidProp], nodes, parents, ++index)
+ return parents
+ }
+ return findParentChainById(node[idProp], tree).map(item => item[idProp])
+}
+
+export function 基本用法 () {
+ const columns: ArtColumn[] = [
+ { code: 'index', name: '索引', width: 80, align: 'center', getValue: (row: any, rowIndex: number): any => rowIndex },
+ { code: 'name', name: '键', width: 200 },
+ { code: 'value', name: '值', width: 200 },
+ ]
+
+ const [useDiv, setUseDiv] = useState(true)
+ const [openKeys, setOpenKeys] = useState([])
+ const [rowIndex, setRowIndex] = useState(0)
+ const [rowKey, setRowKey] = useState('?')
+
+ const tableRef = useRef(null)
+
+ const pipeline = useTablePipeline()
+ .input({ dataSource: dataSource, columns: columns })
+ .primaryKey('id')
+ .use(features.columnResize())
+ .use(features.treeMode({
+ openKeys,
+ onChangeOpenKeys: setOpenKeys,
+ treeColumnCode: 'name'
+ }))
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
setUseDiv(v)}>使用DIV作为容器
+
+
+
+
+ )
+}