From f3b3478506846bfb29f503363216ad260427016e Mon Sep 17 00:00:00 2001 From: chenJJ-88 <118970817+chenJJ-88@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:50:17 +0800 Subject: [PATCH 1/4] feat(useRowSpan): calculate rowspan data based on hierarchical data structure --- config/hooks.ts | 1 + packages/hooks/src/index.ts | 3 +- .../src/useRowSpan/__tests__/index.test.ts | 90 +++++++ packages/hooks/src/useRowSpan/demo/demo1.tsx | 252 ++++++++++++++++++ packages/hooks/src/useRowSpan/demo/demo2.tsx | 186 +++++++++++++ packages/hooks/src/useRowSpan/index.en-US.md | 50 ++++ packages/hooks/src/useRowSpan/index.tsx | 65 +++++ packages/hooks/src/useRowSpan/index.zh-CN.md | 52 ++++ 8 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 packages/hooks/src/useRowSpan/__tests__/index.test.ts create mode 100644 packages/hooks/src/useRowSpan/demo/demo1.tsx create mode 100644 packages/hooks/src/useRowSpan/demo/demo2.tsx create mode 100644 packages/hooks/src/useRowSpan/index.en-US.md create mode 100644 packages/hooks/src/useRowSpan/index.tsx create mode 100644 packages/hooks/src/useRowSpan/index.zh-CN.md diff --git a/config/hooks.ts b/config/hooks.ts index 26ffff428a..d8d6fabd1d 100644 --- a/config/hooks.ts +++ b/config/hooks.ts @@ -32,6 +32,7 @@ export const menus = [ 'useTextSelection', 'useWebSocket', 'useTheme', + 'useRowSpan' ], }, { diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 55c7232b0d..15f1416b4e 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -76,7 +76,7 @@ import useWebSocket from './useWebSocket'; import useWhyDidYouUpdate from './useWhyDidYouUpdate'; import useMutationObserver from './useMutationObserver'; import useTheme from './useTheme'; - +import useRowSpan from './useRowSpan'; export { useRequest, useControllableValue, @@ -158,4 +158,5 @@ export { useResetState, useMutationObserver, useTheme, + useRowSpan }; diff --git a/packages/hooks/src/useRowSpan/__tests__/index.test.ts b/packages/hooks/src/useRowSpan/__tests__/index.test.ts new file mode 100644 index 0000000000..9d6d7ab214 --- /dev/null +++ b/packages/hooks/src/useRowSpan/__tests__/index.test.ts @@ -0,0 +1,90 @@ +import { act, renderHook } from '@testing-library/react'; +import useRowSpan from '../index'; + +describe('useRowSpan', () => { + // 测试基本功能 + it('should return a function when input data is empty', () => { + const { result } = renderHook(() => useRowSpan([], ['key'])); + expect(typeof result.current).toBe('function'); + }); + + // 测试正常情况下的 rowSpan 计算 + it('should calculate rowSpan correctly for same consecutive values', () => { + const data = [ + { id: 1, name: 'A' }, + { id: 2, name: 'A' }, + { id: 3, name: 'B' }, + { id: 4, name: 'A' }, + ]; + + const { result } = renderHook(() => useRowSpan(data, ['name'])); + + // 预期结果:第一行显示rowSpan值,后续相同值的行显示0 + expect(result.current(data[0], 'name')).toEqual({ rowspan: 2 }); + expect(result.current(data[1], 'name')).toEqual({ rowspan: 0 }); + expect(result.current(data[2], 'name')).toEqual({ rowspan: 1 }); + expect(result.current(data[3], 'name')).toEqual({ rowspan: 1 }); + }); + + // 测试所有数据都相同时的情况 + it('should calculate rowSpan correctly when all values are the same', () => { + const data = [ + { id: 1, category: 'X' }, + { id: 2, category: 'X' }, + { id: 3, category: 'X' }, + ]; + + const { result } = renderHook(() => useRowSpan(data, ['category'])); + + expect(result.current(data[0], 'category')).toEqual({ rowspan: 3 }); + expect(result.current(data[1], 'category')).toEqual({ rowspan: 0 }); + expect(result.current(data[2], 'category')).toEqual({ rowspan: 0 }); + }); + + // 测试所有数据都不同情况 + it('should calculate rowSpan correctly when all values are different', () => { + const data = [ + { id: 1, type: 'A' }, + { id: 2, type: 'B' }, + { id: 3, type: 'C' }, + ]; + + const { result } = renderHook(() => useRowSpan(data, ['type'])); + + expect(result.current(data[0], 'type')).toEqual({ rowspan: 1 }); + expect(result.current(data[1], 'type')).toEqual({ rowspan: 1 }); + expect(result.current(data[2], 'type')).toEqual({ rowspan: 1 }); + }); + + // 测试嵌套属性路径 + it('should work with nested property path', () => { + const data = [ + { id: 1, user: { department: 'IT' } }, + { id: 2, user: { department: 'IT' } }, + { id: 3, user: { department: 'HR' } }, + ]; + + const { result } = renderHook(() => useRowSpan(data, ['user.department'])); + + expect(result.current(data[0], 'user.department')).toEqual({ rowspan: 2 }); + expect(result.current(data[1], 'user.department')).toEqual({ rowspan: 0 }); + expect(result.current(data[2], 'user.department')).toEqual({ rowspan: 1 }); + }); + + // 测试包含 undefined 或 null 值的情况 + it('should handle null or undefined values correctly', () => { + const data = [ + { id: 1, value: null }, + { id: 2, value: null }, + { id: 3, value: 'A' }, + { id: 4, value: undefined }, + ]; + + const { result } = renderHook(() => useRowSpan(data, ['value'])); + + expect(result.current(data[0], 'value')).toEqual({ rowspan: 2 }); + expect(result.current(data[1], 'value')).toEqual({ rowspan: 0 }); + expect(result.current(data[2], 'value')).toEqual({ rowspan: 1 }); + expect(result.current(data[3], 'value')).toEqual({ rowspan: 1 }); + }); +}); diff --git a/packages/hooks/src/useRowSpan/demo/demo1.tsx b/packages/hooks/src/useRowSpan/demo/demo1.tsx new file mode 100644 index 0000000000..ebe1124da7 --- /dev/null +++ b/packages/hooks/src/useRowSpan/demo/demo1.tsx @@ -0,0 +1,252 @@ +import React from "react" +import { Table } from "antd" +import { useState, useMemo } from "react" +import { useRowSpan } from "ahooks" +import type { TableProps } from "antd" +interface DataType { + key: string + city: string + school: string + name: string + age: number + six: string +} + +const data: DataType[] = [ + { + city: "四川", + school: "四中", + name: "小红", + age: 18, + six: "女", + key: "1", + }, + { + city: "四川", + school: "四中", + name: "小明", + age: 18, + six: "女", + key: "2", + }, + { + city: "四川", + school: "四中", + name: "小李", + age: 19, + six: "女", + key: "3", + }, + { + city: "四川", + school: "七中", + name: "小王", + age: 20, + six: "女", + key: "4", + }, + { + city: "四川", + school: "七中", + name: "小张", + age: 20, + six: "男", + key: "5", + }, + { + city: "四川", + school: "七中", + name: "小赵", + age: 21, + six: "女", + key: "6", + }, + { + city: "重庆", + school: "七中", + name: "小吴", + age: 24, + six: "男", + key: "7", + }, + { + city: "重庆", + school: "七中", + name: "小周", + age: 24, + six: "女", + key: "8", + }, + { + city: "重庆", + school: "三中", + name: "花花", + age: 22, + six: "女", + key: "9", + }, + { + city: "重庆", + school: "三中", + name: "草草", + age: 22, + six: "男", + key: "10", + }, + { + city: "重庆", + school: "三中", + name: "莹莹", + age: 23, + six: "女", + key: "11", + }, + { + city: "重庆", + school: "一中", + name: "明明", + age: 25, + six: "男", + key: "12", + }, + { + city: "湖北", + school: "六中", + name: "晨晨", + age: 24, + six: "男", + key: "13", + }, + { + city: "湖北", + school: "六中", + name: "亮亮", + age: 24, + six: "男", + key: "14", + }, + { + city: "湖北", + school: "六中", + name: "丽丽", + age: 26, + six: "女", + key: "15", + }, + { + city: "湖北", + school: "一中", + name: "刚刚", + age: 27, + six: "男", + key: "16", + }, + { + city: "湖北", + school: "一中", + name: "芳芳", + age: 27, + six: "女", + key: "17", + }, + { + city: "湖北", + school: "二中", + name: "强强", + age: 28, + six: "男", + key: "18", + }, + { + city: "广东", + school: "中山大学附中", + name: "华华", + age: 25, + six: "女", + key: "19", + }, + { + city: "广东", + school: "深圳中学", + name: "鹏鹏", + age: 26, + six: "男", + key: "20", + }, +] + +const Demo = () => { + const [current, setCurrent] = useState(1) + const [pageSize, setPageSize] = useState(10) + const currentPageData = useMemo(() => { + const startIndex = (current - 1) * pageSize + const endIndex = startIndex + pageSize + return data.slice(startIndex, endIndex) + }, [current, pageSize]) + + const getRowSpan = useRowSpan(data, ["city", "school","six"], currentPageData) + + const columns: TableProps["columns"] = [ + { + title: "城市", + dataIndex: "city", + key: "city", + onCell: (record) => ({ + rowSpan: getRowSpan(record, "city").rowspan, + style: { textAlign: "center", verticalAlign: "middle" }, + }), + }, + { + title: "学校", + dataIndex: "school", + key: "school", + onCell: (record) => ({ + rowSpan: getRowSpan(record, "school").rowspan, + style: { textAlign: "center", verticalAlign: "middle" }, + }), + + }, + { + title: "名字", + dataIndex: "name", + key: "name", + }, + { + title: "年龄", + dataIndex: "age", + key: "age", + }, + { + title: "性别", + dataIndex: "six", + key: "six", + onCell: (record) => ({ + rowSpan: getRowSpan(record, "six").rowspan, + style: { textAlign: "center", verticalAlign: "middle" }, + }), + }, + ] + + return ( + { + setCurrent(page) + setPageSize(size || 10) + }, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`, + }} + /> + ) +} + +export default Demo diff --git a/packages/hooks/src/useRowSpan/demo/demo2.tsx b/packages/hooks/src/useRowSpan/demo/demo2.tsx new file mode 100644 index 0000000000..d146916a7f --- /dev/null +++ b/packages/hooks/src/useRowSpan/demo/demo2.tsx @@ -0,0 +1,186 @@ +import React from "react"; +import { Table } from "antd"; +import { useState, useMemo } from "react"; +import { useRowSpan } from "ahooks" +import type { TableProps } from "antd"; + +interface DataType { + key: string; + city: string; + user: { + school: string; + name: string; + age: number; + six: string; + }; + remark: string; +} + +const data: DataType[] = [ + { + city: "四川", + user: { + school: "四中", + name: "小红", + age: 18, + six: "女", + }, + key: "1", + remark: "备注1", + }, + { + city: "四川", + user: { + school: "四中", + name: "小明", + age: 18, + six: "女", + }, + key: "2", + remark: "备注1", + }, + { + city: "四川", + user: { + school: "七中", + name: "小王", + age: 20, + six: "男", + }, + key: "4", + remark: "备注4", + }, + { + city: "重庆", + user: { + school: "七中", + name: "小吴", + age: 24, + six: "男", + }, + key: "7", + remark: "备注7", + }, + { + city: "重庆", + user: { + school: "三中", + name: "花花", + age: 22, + six: "女", + }, + key: "9", + remark: "备注9", + }, + { + city: "湖北", + user: { + school: "六中", + name: "晨晨", + age: 24, + six: "男", + }, + key: "13", + remark: "备注19", + }, + { + city: "广东", + user: { + school: "中山大学附中", + name: "华华", + age: 25, + six: "女", + }, + key: "19", + remark: "备注19", + }, +]; + +const Demo2 = () => { + const [current, setCurrent] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const currentPageData = useMemo(() => { + const startIndex = (current - 1) * pageSize; + const endIndex = startIndex + pageSize; + return data.slice(startIndex, endIndex); + }, [current, pageSize]); + + const getRowSpan = useRowSpan(data, ["city", "user.school", "user.six","remark"], currentPageData); + + const columns: TableProps["columns"] = [ + { + title: "城市", + dataIndex: "city", + key: "city", + onCell: (record) => ({ + rowSpan: getRowSpan(record, "city").rowspan, + style: { textAlign: "center", verticalAlign: "middle" }, + }), + }, + { + title: "学校", + dataIndex: "user.school", + key: "school", + onCell: (record) => ({ + rowSpan: getRowSpan(record, "user.school").rowspan, + style: { textAlign: "center", verticalAlign: "middle" }, + }), + render:(text,record)=>record.user.school + }, + { + title: "名字", + dataIndex: "user.name", + key: "name", + render: (text, record) => record.user.name + }, + { + title: "年龄", + dataIndex: "user.age", + key: "age", + render: (text, record) => record.user.age + }, + { + title: "性别", + dataIndex: "user.six", + key: "six", + onCell: (record) => ({ + rowSpan: getRowSpan(record, "user.six").rowspan, + style: { textAlign: "center", verticalAlign: "middle" }, + }), + render: (text, record) => record.user.six + }, + { + title: "性别", + dataIndex: "remark", + key: "remark", + onCell: (record) => ({ + rowSpan: getRowSpan(record, "remark").rowspan, + style: { textAlign: "center", verticalAlign: "middle" }, + }), + }, + ]; + + return ( +
{ + setCurrent(page); + setPageSize(size || 10); + }, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`, + }} + /> + ); +}; + +export default Demo2; diff --git a/packages/hooks/src/useRowSpan/index.en-US.md b/packages/hooks/src/useRowSpan/index.en-US.md new file mode 100644 index 0000000000..fedfa11d16 --- /dev/null +++ b/packages/hooks/src/useRowSpan/index.en-US.md @@ -0,0 +1,50 @@ +--- +nav: + path: /hooks +--- + +# useRowSpan + +Generate row span data based on the original data source, suitable for merging rows in tables according to hierarchical levels. + +## Code Demo + +### Basic Usage + + + +## API + +```typescript +const getRowSpan = useRowSpan(allData, ["city", "school", "six"], currentPageData); +``` +### Params + +| Parameter | Description | Type | Default Value | +| --------------- | ---------------------------------------------------------------------- | ---------- | ------------- | +| allData | The full data set to be merged | `T[]` | - | +| hierarchy | Array of hierarchical fields, specifying which fields to merge rows by | `string[]` | `[]` | +| currentPageData | Optional current page data, used for pagination scenarios | `T[]` | `undefined` | + +### Return Value +- Returns a function `getRowSpan` that retrieves the row span information for a specified record and field. + +### getRowSpan Function Parameters +| Parameter | Description | Type | +| --------- | --------------------------------------------------- | -------- | +| record | The current row data record | `T` | +| field | The field for which to get the row span information | `string` | + +### getRowSpan Function Return Value +| Property | Description | Type | +| -------- | -------------------------- | -------- | +| rowspan | The number of rows to span | `number` | + +### Notes + +- The order of fields in the `hierarchy` array determines the merging priority, with earlier fields having higher priority. + +- If `currentPageData` is provided, row span information will only be calculated based on the current page data. + +- This Hook uses `useMemo`, and the calculation will only be re-run when `allData`, `currentPageData`, or `hierarchy` changes. + diff --git a/packages/hooks/src/useRowSpan/index.tsx b/packages/hooks/src/useRowSpan/index.tsx new file mode 100644 index 0000000000..903359527d --- /dev/null +++ b/packages/hooks/src/useRowSpan/index.tsx @@ -0,0 +1,65 @@ +import { useMemo } from "react" + function getNestedValue(record: any, path: string): any { + return path.split('.').reduce((acc, key) => (acc ? acc[key] : undefined), record); +} +export default function useRowSpan>( + data: T[], + hierarchy: string[] = [], + currentPageData?: T[] +) { + const getRowSpan = useMemo(() => { + if (!data || data.length === 0) { + return () => ({ rowspan: 1 }); + } + + const workingData = currentPageData || data; + + const processedData = workingData.map((item, index) => ({ + ...item, + key: item.key || `row-${index}`, + })); + + const spanMap: Record> = {}; + + processedData.forEach(item => { + spanMap[item.key] = {}; + hierarchy.forEach(field => { + spanMap[item.key][field] = { rowspan: 1 }; + }); + }); + + hierarchy.forEach((field, level) => { + let spanCount = 1; + let spanStartIndex = 0; + + for (let i = 0; i < processedData.length; i++) { + const shouldMerge = + i > 0 && + hierarchy + .slice(0, level) + .every(h => getNestedValue(processedData[i], h) === getNestedValue(processedData[i - 1], h)) && + getNestedValue(processedData[i], field) === getNestedValue(processedData[i - 1], field); + + console.log(`Checking merge for field ${field} at index ${i}:`, shouldMerge); + + if (shouldMerge) { + spanCount++; + spanMap[processedData[i].key][field] = { rowspan: 0 }; + } + + if (!shouldMerge || i === processedData.length - 1) { + spanMap[processedData[spanStartIndex].key][field] = { rowspan: spanCount }; + spanCount = 1; + spanStartIndex = i; + } + } + }); + + return (record: T, field: string) => { + const recordKey = record.key || `row-${workingData.indexOf(record)}`; + return spanMap[recordKey]?.[field] || { rowspan: 1 }; + }; + }, [data, currentPageData, hierarchy]); + + return getRowSpan; +} diff --git a/packages/hooks/src/useRowSpan/index.zh-CN.md b/packages/hooks/src/useRowSpan/index.zh-CN.md new file mode 100644 index 0000000000..e07951c39a --- /dev/null +++ b/packages/hooks/src/useRowSpan/index.zh-CN.md @@ -0,0 +1,52 @@ +--- +nav: + path: /hooks +--- + +# useRowSpan + +根据原数据源生成行合并数据,适用于表格中按层级合并行的场景。 + +## 代码演示 + +### 基础用法 + + + +### 数据嵌套 + + +### API + +```typescript +const getRowSpan = useRowSpan(data, ["city", "school", "six"], currentPageData); +``` + +### Params + +| 参数 | 说明 | 类型 | 默认值 | +| --------------- | ------------------- | ---------- | ----------- | +| allData | 需要合并的全量数据 | `T[]` | - | +| hierarchy | 层级字段数组,用于指定按哪些字段合并行。支持嵌套字段路径(如 `user.school`) | `string[]` | `[]` | +| currentPageData | 当前页面数据,可选,用于分页场景 | `T[]` | `undefined` | + + +### 返回值 +- 返回一个函数 `getRowSpan`,用于获取指定记录和字段的行合并信息。 + +### getRowSpan 函数参数 +| 参数 | 说明 | 类型 | +| ------ | ------------ | -------- | +| record | 当前行数据记录 | `T` | +| field | 需要获取行合并信息的字段 | `string` | + +### getRowSpan 函数返回值 +| 属性 | 说明 | 类型 | +| ------- | ------ | -------- | +| rowspan | 行合并的数量 | `number` | + +### 注意事项 + +- `hierarchy` 数组中字段的顺序决定了合并的优先级,靠前的字段优先级更高。 +- 如果提供了 `currentPageData`,则只会基于当前页面数据计算行合并信息。 +- 该 Hook 使用了 `useMemo`,只有当 `allData`、`currentPageData` 或 `hierarchy` 发生变化时才会重新计算。 From 24c723d040648cd99b440379575b58a7c482e353 Mon Sep 17 00:00:00 2001 From: chenJJ-88 <118970817+chenJJ-88@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:51:10 +0800 Subject: [PATCH 2/4] feat(useRowSpan): calculate rowspan data based on hierarchical data structure --- config/hooks.ts | 2 +- packages/hooks/src/index.ts | 2 +- packages/hooks/src/useRowSpan/demo/demo1.tsx | 327 +++++++++---------- packages/hooks/src/useRowSpan/demo/demo2.tsx | 162 ++++----- packages/hooks/src/useRowSpan/index.tsx | 15 +- 5 files changed, 257 insertions(+), 251 deletions(-) diff --git a/config/hooks.ts b/config/hooks.ts index d8d6fabd1d..737253b658 100644 --- a/config/hooks.ts +++ b/config/hooks.ts @@ -32,7 +32,7 @@ export const menus = [ 'useTextSelection', 'useWebSocket', 'useTheme', - 'useRowSpan' + 'useRowSpan', ], }, { diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 15f1416b4e..d1d0d82d96 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -158,5 +158,5 @@ export { useResetState, useMutationObserver, useTheme, - useRowSpan + useRowSpan, }; diff --git a/packages/hooks/src/useRowSpan/demo/demo1.tsx b/packages/hooks/src/useRowSpan/demo/demo1.tsx index ebe1124da7..6133163900 100644 --- a/packages/hooks/src/useRowSpan/demo/demo1.tsx +++ b/packages/hooks/src/useRowSpan/demo/demo1.tsx @@ -1,252 +1,251 @@ -import React from "react" -import { Table } from "antd" -import { useState, useMemo } from "react" -import { useRowSpan } from "ahooks" -import type { TableProps } from "antd" +import React from 'react'; +import { Table } from 'antd'; +import { useState, useMemo } from 'react'; +import { useRowSpan } from 'ahooks'; +import type { TableProps } from 'antd'; interface DataType { - key: string - city: string - school: string - name: string - age: number - six: string + key: string; + city: string; + school: string; + name: string; + age: number; + six: string; } const data: DataType[] = [ { - city: "四川", - school: "四中", - name: "小红", + city: '四川', + school: '四中', + name: '小红', age: 18, - six: "女", - key: "1", + six: '女', + key: '1', }, { - city: "四川", - school: "四中", - name: "小明", + city: '四川', + school: '四中', + name: '小明', age: 18, - six: "女", - key: "2", + six: '女', + key: '2', }, { - city: "四川", - school: "四中", - name: "小李", + city: '四川', + school: '四中', + name: '小李', age: 19, - six: "女", - key: "3", + six: '女', + key: '3', }, { - city: "四川", - school: "七中", - name: "小王", + city: '四川', + school: '七中', + name: '小王', age: 20, - six: "女", - key: "4", + six: '女', + key: '4', }, { - city: "四川", - school: "七中", - name: "小张", + city: '四川', + school: '七中', + name: '小张', age: 20, - six: "男", - key: "5", + six: '男', + key: '5', }, { - city: "四川", - school: "七中", - name: "小赵", + city: '四川', + school: '七中', + name: '小赵', age: 21, - six: "女", - key: "6", + six: '女', + key: '6', }, { - city: "重庆", - school: "七中", - name: "小吴", + city: '重庆', + school: '七中', + name: '小吴', age: 24, - six: "男", - key: "7", + six: '男', + key: '7', }, { - city: "重庆", - school: "七中", - name: "小周", + city: '重庆', + school: '七中', + name: '小周', age: 24, - six: "女", - key: "8", + six: '女', + key: '8', }, { - city: "重庆", - school: "三中", - name: "花花", + city: '重庆', + school: '三中', + name: '花花', age: 22, - six: "女", - key: "9", + six: '女', + key: '9', }, { - city: "重庆", - school: "三中", - name: "草草", + city: '重庆', + school: '三中', + name: '草草', age: 22, - six: "男", - key: "10", + six: '男', + key: '10', }, { - city: "重庆", - school: "三中", - name: "莹莹", + city: '重庆', + school: '三中', + name: '莹莹', age: 23, - six: "女", - key: "11", + six: '女', + key: '11', }, { - city: "重庆", - school: "一中", - name: "明明", + city: '重庆', + school: '一中', + name: '明明', age: 25, - six: "男", - key: "12", + six: '男', + key: '12', }, { - city: "湖北", - school: "六中", - name: "晨晨", + city: '湖北', + school: '六中', + name: '晨晨', age: 24, - six: "男", - key: "13", + six: '男', + key: '13', }, { - city: "湖北", - school: "六中", - name: "亮亮", + city: '湖北', + school: '六中', + name: '亮亮', age: 24, - six: "男", - key: "14", + six: '男', + key: '14', }, { - city: "湖北", - school: "六中", - name: "丽丽", + city: '湖北', + school: '六中', + name: '丽丽', age: 26, - six: "女", - key: "15", + six: '女', + key: '15', }, { - city: "湖北", - school: "一中", - name: "刚刚", + city: '湖北', + school: '一中', + name: '刚刚', age: 27, - six: "男", - key: "16", + six: '男', + key: '16', }, { - city: "湖北", - school: "一中", - name: "芳芳", + city: '湖北', + school: '一中', + name: '芳芳', age: 27, - six: "女", - key: "17", + six: '女', + key: '17', }, { - city: "湖北", - school: "二中", - name: "强强", + city: '湖北', + school: '二中', + name: '强强', age: 28, - six: "男", - key: "18", + six: '男', + key: '18', }, { - city: "广东", - school: "中山大学附中", - name: "华华", + city: '广东', + school: '中山大学附中', + name: '华华', age: 25, - six: "女", - key: "19", + six: '女', + key: '19', }, { - city: "广东", - school: "深圳中学", - name: "鹏鹏", + city: '广东', + school: '深圳中学', + name: '鹏鹏', age: 26, - six: "男", - key: "20", + six: '男', + key: '20', }, -] +]; const Demo = () => { - const [current, setCurrent] = useState(1) - const [pageSize, setPageSize] = useState(10) + const [current, setCurrent] = useState(1); + const [pageSize, setPageSize] = useState(10); const currentPageData = useMemo(() => { - const startIndex = (current - 1) * pageSize - const endIndex = startIndex + pageSize - return data.slice(startIndex, endIndex) - }, [current, pageSize]) + const startIndex = (current - 1) * pageSize; + const endIndex = startIndex + pageSize; + return data.slice(startIndex, endIndex); + }, [current, pageSize]); - const getRowSpan = useRowSpan(data, ["city", "school","six"], currentPageData) + const getRowSpan = useRowSpan(data, ['city', 'school', 'six'], currentPageData); - const columns: TableProps["columns"] = [ + const columns: TableProps['columns'] = [ { - title: "城市", - dataIndex: "city", - key: "city", + title: '城市', + dataIndex: 'city', + key: 'city', onCell: (record) => ({ - rowSpan: getRowSpan(record, "city").rowspan, - style: { textAlign: "center", verticalAlign: "middle" }, + rowSpan: getRowSpan(record, 'city').rowspan, + style: { textAlign: 'center', verticalAlign: 'middle' }, }), }, { - title: "学校", - dataIndex: "school", - key: "school", + title: '学校', + dataIndex: 'school', + key: 'school', onCell: (record) => ({ - rowSpan: getRowSpan(record, "school").rowspan, - style: { textAlign: "center", verticalAlign: "middle" }, + rowSpan: getRowSpan(record, 'school').rowspan, + style: { textAlign: 'center', verticalAlign: 'middle' }, }), - }, { - title: "名字", - dataIndex: "name", - key: "name", + title: '名字', + dataIndex: 'name', + key: 'name', }, { - title: "年龄", - dataIndex: "age", - key: "age", + title: '年龄', + dataIndex: 'age', + key: 'age', }, { - title: "性别", - dataIndex: "six", - key: "six", + title: '性别', + dataIndex: 'six', + key: 'six', onCell: (record) => ({ - rowSpan: getRowSpan(record, "six").rowspan, - style: { textAlign: "center", verticalAlign: "middle" }, + rowSpan: getRowSpan(record, 'six').rowspan, + style: { textAlign: 'center', verticalAlign: 'middle' }, }), }, - ] + ]; return ( -
{ - setCurrent(page) - setPageSize(size || 10) - }, - showSizeChanger: true, - showQuickJumper: true, - showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`, - }} - /> - ) -} +
{ + setCurrent(page); + setPageSize(size || 10); + }, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`, + }} + /> + ); +}; -export default Demo +export default Demo; diff --git a/packages/hooks/src/useRowSpan/demo/demo2.tsx b/packages/hooks/src/useRowSpan/demo/demo2.tsx index d146916a7f..8d1fe5efba 100644 --- a/packages/hooks/src/useRowSpan/demo/demo2.tsx +++ b/packages/hooks/src/useRowSpan/demo/demo2.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import { Table } from "antd"; -import { useState, useMemo } from "react"; -import { useRowSpan } from "ahooks" -import type { TableProps } from "antd"; +import React from 'react'; +import { Table } from 'antd'; +import { useState, useMemo } from 'react'; +import { useRowSpan } from 'ahooks'; +import type { TableProps } from 'antd'; interface DataType { key: string; @@ -18,81 +18,81 @@ interface DataType { const data: DataType[] = [ { - city: "四川", + city: '四川', user: { - school: "四中", - name: "小红", + school: '四中', + name: '小红', age: 18, - six: "女", + six: '女', }, - key: "1", - remark: "备注1", + key: '1', + remark: '备注1', }, { - city: "四川", + city: '四川', user: { - school: "四中", - name: "小明", + school: '四中', + name: '小明', age: 18, - six: "女", + six: '女', }, - key: "2", - remark: "备注1", + key: '2', + remark: '备注1', }, { - city: "四川", + city: '四川', user: { - school: "七中", - name: "小王", + school: '七中', + name: '小王', age: 20, - six: "男", + six: '男', }, - key: "4", - remark: "备注4", + key: '4', + remark: '备注4', }, { - city: "重庆", + city: '重庆', user: { - school: "七中", - name: "小吴", + school: '七中', + name: '小吴', age: 24, - six: "男", + six: '男', }, - key: "7", - remark: "备注7", + key: '7', + remark: '备注7', }, { - city: "重庆", + city: '重庆', user: { - school: "三中", - name: "花花", + school: '三中', + name: '花花', age: 22, - six: "女", + six: '女', }, - key: "9", - remark: "备注9", + key: '9', + remark: '备注9', }, { - city: "湖北", + city: '湖北', user: { - school: "六中", - name: "晨晨", + school: '六中', + name: '晨晨', age: 24, - six: "男", + six: '男', }, - key: "13", - remark: "备注19", + key: '13', + remark: '备注19', }, { - city: "广东", + city: '广东', user: { - school: "中山大学附中", - name: "华华", + school: '中山大学附中', + name: '华华', age: 25, - six: "女", + six: '女', }, - key: "19", - remark: "备注19", + key: '19', + remark: '备注19', }, ]; @@ -106,57 +106,61 @@ const Demo2 = () => { return data.slice(startIndex, endIndex); }, [current, pageSize]); - const getRowSpan = useRowSpan(data, ["city", "user.school", "user.six","remark"], currentPageData); + const getRowSpan = useRowSpan( + data, + ['city', 'user.school', 'user.six', 'remark'], + currentPageData, + ); - const columns: TableProps["columns"] = [ + const columns: TableProps['columns'] = [ { - title: "城市", - dataIndex: "city", - key: "city", + title: '城市', + dataIndex: 'city', + key: 'city', onCell: (record) => ({ - rowSpan: getRowSpan(record, "city").rowspan, - style: { textAlign: "center", verticalAlign: "middle" }, + rowSpan: getRowSpan(record, 'city').rowspan, + style: { textAlign: 'center', verticalAlign: 'middle' }, }), }, { - title: "学校", - dataIndex: "user.school", - key: "school", + title: '学校', + dataIndex: 'user.school', + key: 'school', onCell: (record) => ({ - rowSpan: getRowSpan(record, "user.school").rowspan, - style: { textAlign: "center", verticalAlign: "middle" }, + rowSpan: getRowSpan(record, 'user.school').rowspan, + style: { textAlign: 'center', verticalAlign: 'middle' }, }), - render:(text,record)=>record.user.school + render: (text, record) => record.user.school, }, { - title: "名字", - dataIndex: "user.name", - key: "name", - render: (text, record) => record.user.name + title: '名字', + dataIndex: 'user.name', + key: 'name', + render: (text, record) => record.user.name, }, { - title: "年龄", - dataIndex: "user.age", - key: "age", - render: (text, record) => record.user.age + title: '年龄', + dataIndex: 'user.age', + key: 'age', + render: (text, record) => record.user.age, }, { - title: "性别", - dataIndex: "user.six", - key: "six", + title: '性别', + dataIndex: 'user.six', + key: 'six', onCell: (record) => ({ - rowSpan: getRowSpan(record, "user.six").rowspan, - style: { textAlign: "center", verticalAlign: "middle" }, + rowSpan: getRowSpan(record, 'user.six').rowspan, + style: { textAlign: 'center', verticalAlign: 'middle' }, }), - render: (text, record) => record.user.six + render: (text, record) => record.user.six, }, { - title: "性别", - dataIndex: "remark", - key: "remark", + title: '性别', + dataIndex: 'remark', + key: 'remark', onCell: (record) => ({ - rowSpan: getRowSpan(record, "remark").rowspan, - style: { textAlign: "center", verticalAlign: "middle" }, + rowSpan: getRowSpan(record, 'remark').rowspan, + style: { textAlign: 'center', verticalAlign: 'middle' }, }), }, ]; diff --git a/packages/hooks/src/useRowSpan/index.tsx b/packages/hooks/src/useRowSpan/index.tsx index 903359527d..a668a58c5d 100644 --- a/packages/hooks/src/useRowSpan/index.tsx +++ b/packages/hooks/src/useRowSpan/index.tsx @@ -1,11 +1,11 @@ -import { useMemo } from "react" - function getNestedValue(record: any, path: string): any { +import { useMemo } from 'react'; +function getNestedValue(record: any, path: string): any { return path.split('.').reduce((acc, key) => (acc ? acc[key] : undefined), record); } export default function useRowSpan>( data: T[], hierarchy: string[] = [], - currentPageData?: T[] + currentPageData?: T[], ) { const getRowSpan = useMemo(() => { if (!data || data.length === 0) { @@ -21,9 +21,9 @@ export default function useRowSpan>( const spanMap: Record> = {}; - processedData.forEach(item => { + processedData.forEach((item) => { spanMap[item.key] = {}; - hierarchy.forEach(field => { + hierarchy.forEach((field) => { spanMap[item.key][field] = { rowspan: 1 }; }); }); @@ -37,7 +37,10 @@ export default function useRowSpan>( i > 0 && hierarchy .slice(0, level) - .every(h => getNestedValue(processedData[i], h) === getNestedValue(processedData[i - 1], h)) && + .every( + (h) => + getNestedValue(processedData[i], h) === getNestedValue(processedData[i - 1], h), + ) && getNestedValue(processedData[i], field) === getNestedValue(processedData[i - 1], field); console.log(`Checking merge for field ${field} at index ${i}:`, shouldMerge); From 6821fcc161ab6497eabdcf0d646e39ff2b64aad7 Mon Sep 17 00:00:00 2001 From: chenJJ-88 <118970817+chenJJ-88@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:05:30 +0800 Subject: [PATCH 3/4] feat(useRowSpan): calculate rowspan data based on hierarchical data structure --- packages/hooks/src/useRowSpan/demo/demo1.tsx | 50 ++++++++++---------- packages/hooks/src/useRowSpan/demo/demo2.tsx | 26 +++++----- packages/hooks/src/useRowSpan/index.en-US.md | 2 +- packages/hooks/src/useRowSpan/index.zh-CN.md | 2 +- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/hooks/src/useRowSpan/demo/demo1.tsx b/packages/hooks/src/useRowSpan/demo/demo1.tsx index 6133163900..ada222ada7 100644 --- a/packages/hooks/src/useRowSpan/demo/demo1.tsx +++ b/packages/hooks/src/useRowSpan/demo/demo1.tsx @@ -9,7 +9,7 @@ interface DataType { school: string; name: string; age: number; - six: string; + gender: string; } const data: DataType[] = [ @@ -18,7 +18,7 @@ const data: DataType[] = [ school: '四中', name: '小红', age: 18, - six: '女', + gender: '女', key: '1', }, { @@ -26,7 +26,7 @@ const data: DataType[] = [ school: '四中', name: '小明', age: 18, - six: '女', + gender: '女', key: '2', }, { @@ -34,7 +34,7 @@ const data: DataType[] = [ school: '四中', name: '小李', age: 19, - six: '女', + gender: '女', key: '3', }, { @@ -42,7 +42,7 @@ const data: DataType[] = [ school: '七中', name: '小王', age: 20, - six: '女', + gender: '女', key: '4', }, { @@ -50,7 +50,7 @@ const data: DataType[] = [ school: '七中', name: '小张', age: 20, - six: '男', + gender: '男', key: '5', }, { @@ -58,7 +58,7 @@ const data: DataType[] = [ school: '七中', name: '小赵', age: 21, - six: '女', + gender: '女', key: '6', }, { @@ -66,7 +66,7 @@ const data: DataType[] = [ school: '七中', name: '小吴', age: 24, - six: '男', + gender: '男', key: '7', }, { @@ -74,7 +74,7 @@ const data: DataType[] = [ school: '七中', name: '小周', age: 24, - six: '女', + gender: '女', key: '8', }, { @@ -82,7 +82,7 @@ const data: DataType[] = [ school: '三中', name: '花花', age: 22, - six: '女', + gender: '女', key: '9', }, { @@ -90,7 +90,7 @@ const data: DataType[] = [ school: '三中', name: '草草', age: 22, - six: '男', + gender: '男', key: '10', }, { @@ -98,7 +98,7 @@ const data: DataType[] = [ school: '三中', name: '莹莹', age: 23, - six: '女', + gender: '女', key: '11', }, { @@ -106,7 +106,7 @@ const data: DataType[] = [ school: '一中', name: '明明', age: 25, - six: '男', + gender: '男', key: '12', }, { @@ -114,7 +114,7 @@ const data: DataType[] = [ school: '六中', name: '晨晨', age: 24, - six: '男', + gender: '男', key: '13', }, { @@ -122,7 +122,7 @@ const data: DataType[] = [ school: '六中', name: '亮亮', age: 24, - six: '男', + gender: '男', key: '14', }, { @@ -130,7 +130,7 @@ const data: DataType[] = [ school: '六中', name: '丽丽', age: 26, - six: '女', + gender: '女', key: '15', }, { @@ -138,7 +138,7 @@ const data: DataType[] = [ school: '一中', name: '刚刚', age: 27, - six: '男', + gender: '男', key: '16', }, { @@ -146,7 +146,7 @@ const data: DataType[] = [ school: '一中', name: '芳芳', age: 27, - six: '女', + gender: '女', key: '17', }, { @@ -154,7 +154,7 @@ const data: DataType[] = [ school: '二中', name: '强强', age: 28, - six: '男', + gender: '男', key: '18', }, { @@ -162,7 +162,7 @@ const data: DataType[] = [ school: '中山大学附中', name: '华华', age: 25, - six: '女', + gender: '女', key: '19', }, { @@ -170,7 +170,7 @@ const data: DataType[] = [ school: '深圳中学', name: '鹏鹏', age: 26, - six: '男', + gender: '男', key: '20', }, ]; @@ -184,7 +184,7 @@ const Demo = () => { return data.slice(startIndex, endIndex); }, [current, pageSize]); - const getRowSpan = useRowSpan(data, ['city', 'school', 'six'], currentPageData); + const getRowSpan = useRowSpan(data, ['city', 'school', 'gender'], currentPageData); const columns: TableProps['columns'] = [ { @@ -217,10 +217,10 @@ const Demo = () => { }, { title: '性别', - dataIndex: 'six', - key: 'six', + dataIndex: 'gender', + key: 'gender', onCell: (record) => ({ - rowSpan: getRowSpan(record, 'six').rowspan, + rowSpan: getRowSpan(record, 'gender').rowspan, style: { textAlign: 'center', verticalAlign: 'middle' }, }), }, diff --git a/packages/hooks/src/useRowSpan/demo/demo2.tsx b/packages/hooks/src/useRowSpan/demo/demo2.tsx index 8d1fe5efba..e0ed05af8c 100644 --- a/packages/hooks/src/useRowSpan/demo/demo2.tsx +++ b/packages/hooks/src/useRowSpan/demo/demo2.tsx @@ -11,7 +11,7 @@ interface DataType { school: string; name: string; age: number; - six: string; + gender: string; }; remark: string; } @@ -23,7 +23,7 @@ const data: DataType[] = [ school: '四中', name: '小红', age: 18, - six: '女', + gender: '女', }, key: '1', remark: '备注1', @@ -34,7 +34,7 @@ const data: DataType[] = [ school: '四中', name: '小明', age: 18, - six: '女', + gender: '女', }, key: '2', remark: '备注1', @@ -45,7 +45,7 @@ const data: DataType[] = [ school: '七中', name: '小王', age: 20, - six: '男', + gender: '男', }, key: '4', remark: '备注4', @@ -56,7 +56,7 @@ const data: DataType[] = [ school: '七中', name: '小吴', age: 24, - six: '男', + gender: '男', }, key: '7', remark: '备注7', @@ -67,7 +67,7 @@ const data: DataType[] = [ school: '三中', name: '花花', age: 22, - six: '女', + gender: '女', }, key: '9', remark: '备注9', @@ -78,7 +78,7 @@ const data: DataType[] = [ school: '六中', name: '晨晨', age: 24, - six: '男', + gender: '男', }, key: '13', remark: '备注19', @@ -89,7 +89,7 @@ const data: DataType[] = [ school: '中山大学附中', name: '华华', age: 25, - six: '女', + gender: '女', }, key: '19', remark: '备注19', @@ -108,7 +108,7 @@ const Demo2 = () => { const getRowSpan = useRowSpan( data, - ['city', 'user.school', 'user.six', 'remark'], + ['city', 'user.school', 'user.gender', 'remark'], currentPageData, ); @@ -146,13 +146,13 @@ const Demo2 = () => { }, { title: '性别', - dataIndex: 'user.six', - key: 'six', + dataIndex: 'user.gender', + key: 'gender', onCell: (record) => ({ - rowSpan: getRowSpan(record, 'user.six').rowspan, + rowSpan: getRowSpan(record, 'user.gender').rowspan, style: { textAlign: 'center', verticalAlign: 'middle' }, }), - render: (text, record) => record.user.six, + render: (text, record) => record.user.gender, }, { title: '性别', diff --git a/packages/hooks/src/useRowSpan/index.en-US.md b/packages/hooks/src/useRowSpan/index.en-US.md index fedfa11d16..296caaeff3 100644 --- a/packages/hooks/src/useRowSpan/index.en-US.md +++ b/packages/hooks/src/useRowSpan/index.en-US.md @@ -16,7 +16,7 @@ Generate row span data based on the original data source, suitable for merging r ## API ```typescript -const getRowSpan = useRowSpan(allData, ["city", "school", "six"], currentPageData); +const getRowSpan = useRowSpan(allData, ["city", "school", "gender"], currentPageData); ``` ### Params diff --git a/packages/hooks/src/useRowSpan/index.zh-CN.md b/packages/hooks/src/useRowSpan/index.zh-CN.md index e07951c39a..ca4f468677 100644 --- a/packages/hooks/src/useRowSpan/index.zh-CN.md +++ b/packages/hooks/src/useRowSpan/index.zh-CN.md @@ -19,7 +19,7 @@ nav: ### API ```typescript -const getRowSpan = useRowSpan(data, ["city", "school", "six"], currentPageData); +const getRowSpan = useRowSpan(data, ["city", "school", "gender"], currentPageData); ``` ### Params From ca3158257e81c5836f8f0887d176acb494719ec3 Mon Sep 17 00:00:00 2001 From: chenJJ-88 <118970817+chenJJ-88@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:31:23 +0800 Subject: [PATCH 4/4] test: add vitest for useRowSpan hook --- .../index.test.ts => tests/index.spec.ts} | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) rename packages/hooks/src/useRowSpan/{__tests__/index.test.ts => tests/index.spec.ts} (83%) diff --git a/packages/hooks/src/useRowSpan/__tests__/index.test.ts b/packages/hooks/src/useRowSpan/tests/index.spec.ts similarity index 83% rename from packages/hooks/src/useRowSpan/__tests__/index.test.ts rename to packages/hooks/src/useRowSpan/tests/index.spec.ts index 9d6d7ab214..655dacf073 100644 --- a/packages/hooks/src/useRowSpan/__tests__/index.test.ts +++ b/packages/hooks/src/useRowSpan/tests/index.spec.ts @@ -1,15 +1,16 @@ -import { act, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; import useRowSpan from '../index'; describe('useRowSpan', () => { // 测试基本功能 - it('should return a function when input data is empty', () => { + test('should return a function when input data is empty', () => { const { result } = renderHook(() => useRowSpan([], ['key'])); expect(typeof result.current).toBe('function'); }); // 测试正常情况下的 rowSpan 计算 - it('should calculate rowSpan correctly for same consecutive values', () => { + test('should calculate rowSpan correctly for same consecutive values', () => { const data = [ { id: 1, name: 'A' }, { id: 2, name: 'A' }, @@ -27,7 +28,7 @@ describe('useRowSpan', () => { }); // 测试所有数据都相同时的情况 - it('should calculate rowSpan correctly when all values are the same', () => { + test('should calculate rowSpan correctly when all values are the same', () => { const data = [ { id: 1, category: 'X' }, { id: 2, category: 'X' }, @@ -42,7 +43,7 @@ describe('useRowSpan', () => { }); // 测试所有数据都不同情况 - it('should calculate rowSpan correctly when all values are different', () => { + test('should calculate rowSpan correctly when all values are different', () => { const data = [ { id: 1, type: 'A' }, { id: 2, type: 'B' }, @@ -57,7 +58,7 @@ describe('useRowSpan', () => { }); // 测试嵌套属性路径 - it('should work with nested property path', () => { + test('should work with nested property path', () => { const data = [ { id: 1, user: { department: 'IT' } }, { id: 2, user: { department: 'IT' } }, @@ -72,7 +73,7 @@ describe('useRowSpan', () => { }); // 测试包含 undefined 或 null 值的情况 - it('should handle null or undefined values correctly', () => { + test('should handle null or undefined values correctly', () => { const data = [ { id: 1, value: null }, { id: 2, value: null },