Skip to content

refactor(pagination): data abstraction via hooks #2988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 12, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions src/hooks/use-pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
interface PaginationOptions {
// 当前页码, 从 1 开始
current: number
// 数据总条数
total: number
// 每页显示的条目数
itemsPerPage: number
// 指示器条目数量
displayCount: number
// 省略符号
ellipse: boolean
}

export type PaginationNode = {
number: number
text: string
selected?: boolean
}

type PaginationResult = [
// 指示器
PaginationNodes: Array<PaginationNode>,
// 指示器的总数量
PaginationNodesCount: number,
]

const defaultPaginationOptions: Partial<PaginationOptions> = {
current: 0,
itemsPerPage: 10,
displayCount: 5,
ellipse: false,
}
Comment on lines +27 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修正默认值与接口定义的不一致

根据接口注释,current 应该从 1 开始计数,但默认值却设为了 0。这与接口定义不符,可能导致计算错误。

const defaultPaginationOptions: Partial<PaginationOptions> = {
-  current: 0,
+  current: 1,
  itemsPerPage: 10,
  displayCount: 5,
  ellipse: false,
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultPaginationOptions: Partial<PaginationOptions> = {
current: 0,
itemsPerPage: 10,
displayCount: 5,
ellipse: false,
}
const defaultPaginationOptions: Partial<PaginationOptions> = {
current: 1,
itemsPerPage: 10,
displayCount: 5,
ellipse: false,
}


function human2Machine(number: number) {
return --number
}

function calculateNodes(options: PaginationOptions, nodesCount: number) {
// 分页器内部的索引从 0 开始,用户使用的索引从 1 开始
const halfIndex = Math.floor(options.displayCount / 2)
const buttonsCountIndex = human2Machine(nodesCount)
const displayCountIndex = human2Machine(options.displayCount)
const currentIndex = human2Machine(options.current)
let start
let end
if (buttonsCountIndex <= displayCountIndex) {
start = 0
end = buttonsCountIndex
} else {
start = Math.max(0, currentIndex - halfIndex)
end = Math.min(buttonsCountIndex, currentIndex + halfIndex)
if (end - start < displayCountIndex) {
if (start === 0) {
end = Math.min(start + displayCountIndex, buttonsCountIndex)
} else if (end === buttonsCountIndex) {
start = Math.max(end - displayCountIndex, 1)
}

Check warning on line 57 in src/hooks/use-pagination.ts

Codecov / codecov/patch

src/hooks/use-pagination.ts#L56-L57

Added lines #L56 - L57 were not covered by tests
} else if (end - start > displayCountIndex) {
end = start + displayCountIndex
}

Check warning on line 60 in src/hooks/use-pagination.ts

Codecov / codecov/patch

src/hooks/use-pagination.ts#L59-L60

Added lines #L59 - L60 were not covered by tests
}

const buttons = []
for (let i = start; i <= end; i++) {
const humanPageNumber = i + 1
buttons.push({
number: humanPageNumber,
text: humanPageNumber.toString(),
selected: options.current === humanPageNumber,
})
}

return addEllipses(buttons, {
nodesCount,
ellipse: options.ellipse,
displayCount: options.displayCount,
})
}
Comment on lines +38 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

优化 calculateNodes 函数

calculateNodes 函数有几个需要注意的地方:

  1. 函数内部调用 human2Machine 的地方应更新为新的函数名
  2. 第 56 行的 Math.max(end - displayCountIndex, 1) 中,最小值应为 0 而非 1,因为索引是从 0 开始的
  3. 缺少对 options.displayCount 为 0 或负数的验证,可能导致计算错误

同时,根据静态分析,第 56-57 行和 59-60 行代码未被测试覆盖,建议增加相应测试用例。

function calculateNodes(options: PaginationOptions, nodesCount: number) {
  // 分页器内部的索引从 0 开始,用户使用的索引从 1 开始
  const halfIndex = Math.floor(options.displayCount / 2)
-  const buttonsCountIndex = human2Machine(nodesCount)
-  const displayCountIndex = human2Machine(options.displayCount)
-  const currentIndex = human2Machine(options.current)
+  const buttonsCountIndex = oneBasedToZeroBased(nodesCount)
+  const displayCountIndex = oneBasedToZeroBased(options.displayCount)
+  const currentIndex = oneBasedToZeroBased(options.current)

+  if (options.displayCount <= 0) {
+    throw new Error('displayCount must be greater than 0')
+  }

  let start
  let end
  if (buttonsCountIndex <= displayCountIndex) {
    start = 0
    end = buttonsCountIndex
  } else {
    start = Math.max(0, currentIndex - halfIndex)
    end = Math.min(buttonsCountIndex, currentIndex + halfIndex)
    if (end - start < displayCountIndex) {
      if (start === 0) {
        end = Math.min(start + displayCountIndex, buttonsCountIndex)
      } else if (end === buttonsCountIndex) {
-        start = Math.max(end - displayCountIndex, 1)
+        start = Math.max(end - displayCountIndex, 0)
      }
    } else if (end - start > displayCountIndex) {
      end = start + displayCountIndex
    }
  }

  const buttons = []
  for (let i = start; i <= end; i++) {
    const humanPageNumber = i + 1
    buttons.push({
      number: humanPageNumber,
      text: humanPageNumber.toString(),
      selected: options.current === humanPageNumber,
    })
  }

  return addEllipses(buttons, {
    nodesCount,
    ellipse: options.ellipse,
    displayCount: options.displayCount,
  })
}

请运行以下脚本验证未覆盖的代码路径:


🏁 Script executed:

#!/bin/bash
echo "检查未覆盖的代码路径..."
echo "查找测试文件:"
fd -t f --exec grep -l "usePagination" {} \; | grep test
echo "查找测试覆盖率报告:"
fd -t f -e json coverage

Length of output: 457


修正 calculateNodes 函数调用和边界验证问题

  • 请将函数内部调用 human2Machine 替换为 oneBasedToZeroBased 以保持一致;
  • 对于第 56 行的调整,修改 Math.max(end - displayCountIndex, 1)Math.max(end - displayCountIndex, 0),确保索引从 0 开始;
  • 增加对 options.displayCount 参数的有效性验证(仅允许正数),防止出现计算错误;
  • 注意:当前代码中第 56-57 行以及第 59-60 行的逻辑没有对应测试覆盖,请补充相应的单元测试确保这部分逻辑的正确性。

Diff 供参考:

function calculateNodes(options: PaginationOptions, nodesCount: number) {
  // 分页器内部的索引从 0 开始,用户使用的索引从 1 开始
-  const buttonsCountIndex = human2Machine(nodesCount)
-  const displayCountIndex = human2Machine(options.displayCount)
-  const currentIndex = human2Machine(options.current)
+  const buttonsCountIndex = oneBasedToZeroBased(nodesCount)
+  const displayCountIndex = oneBasedToZeroBased(options.displayCount)
+  const currentIndex = oneBasedToZeroBased(options.current)

+  if (options.displayCount <= 0) {
+    throw new Error('displayCount must be greater than 0')
+  }

  let start
  let end
  if (buttonsCountIndex <= displayCountIndex) {
    start = 0
    end = buttonsCountIndex
  } else {
    start = Math.max(0, currentIndex - halfIndex)
    end = Math.min(buttonsCountIndex, currentIndex + halfIndex)
    if (end - start < displayCountIndex) {
      if (start === 0) {
        end = Math.min(start + displayCountIndex, buttonsCountIndex)
      } else if (end === buttonsCountIndex) {
-        start = Math.max(end - displayCountIndex, 1)
+        start = Math.max(end - displayCountIndex, 0)
      }
    } else if (end - start > displayCountIndex) {
      end = start + displayCountIndex
    }
  }
  // ... 省略后续代码
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function calculateNodes(options: PaginationOptions, nodesCount: number) {
// 分页器内部的索引从 0 开始,用户使用的索引从 1 开始
const halfIndex = Math.floor(options.displayCount / 2)
const buttonsCountIndex = human2Machine(nodesCount)
const displayCountIndex = human2Machine(options.displayCount)
const currentIndex = human2Machine(options.current)
let start
let end
if (buttonsCountIndex <= displayCountIndex) {
start = 0
end = buttonsCountIndex
} else {
start = Math.max(0, currentIndex - halfIndex)
end = Math.min(buttonsCountIndex, currentIndex + halfIndex)
if (end - start < displayCountIndex) {
if (start === 0) {
end = Math.min(start + displayCountIndex, buttonsCountIndex)
} else if (end === buttonsCountIndex) {
start = Math.max(end - displayCountIndex, 1)
}
} else if (end - start > displayCountIndex) {
end = start + displayCountIndex
}
}
const buttons = []
for (let i = start; i <= end; i++) {
const humanPageNumber = i + 1
buttons.push({
number: humanPageNumber,
text: humanPageNumber.toString(),
selected: options.current === humanPageNumber,
})
}
return addEllipses(buttons, {
nodesCount,
ellipse: options.ellipse,
displayCount: options.displayCount,
})
}
function calculateNodes(options: PaginationOptions, nodesCount: number) {
// 分页器内部的索引从 0 开始,用户使用的索引从 1 开始
const halfIndex = Math.floor(options.displayCount / 2)
const buttonsCountIndex = oneBasedToZeroBased(nodesCount)
const displayCountIndex = oneBasedToZeroBased(options.displayCount)
const currentIndex = oneBasedToZeroBased(options.current)
if (options.displayCount <= 0) {
throw new Error('displayCount must be greater than 0')
}
let start
let end
if (buttonsCountIndex <= displayCountIndex) {
start = 0
end = buttonsCountIndex
} else {
start = Math.max(0, currentIndex - halfIndex)
end = Math.min(buttonsCountIndex, currentIndex + halfIndex)
if (end - start < displayCountIndex) {
if (start === 0) {
end = Math.min(start + displayCountIndex, buttonsCountIndex)
} else if (end === buttonsCountIndex) {
start = Math.max(end - displayCountIndex, 0)
}
} else if (end - start > displayCountIndex) {
end = start + displayCountIndex
}
}
const buttons = []
for (let i = start; i <= end; i++) {
const humanPageNumber = i + 1
buttons.push({
number: humanPageNumber,
text: humanPageNumber.toString(),
selected: options.current === humanPageNumber,
})
}
return addEllipses(buttons, {
nodesCount,
ellipse: options.ellipse,
displayCount: options.displayCount,
})
}
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 56-57: src/hooks/use-pagination.ts#L56-L57
Added lines #L56 - L57 were not covered by tests


[warning] 59-60: src/hooks/use-pagination.ts#L59-L60
Added lines #L59 - L60 were not covered by tests


function addEllipses(
nodes: Array<PaginationNode>,
{
displayCount,
nodesCount,
ellipse,
}: { displayCount: number; nodesCount: number; ellipse: boolean }
) {
if (nodesCount <= displayCount || !ellipse) return nodes
const start = nodes[0]
const end = nodes[nodes.length - 1]

const leftEllipse = start.number > 1
const rightEllipse = end.number < nodesCount
if (leftEllipse) {
nodes.unshift({ number: start.number - 1, text: '...' })
}
if (rightEllipse) {
nodes.push({ number: end.number + 1, text: '...' })
}
return nodes
}

export const usePagination = (options: PaginationOptions): PaginationResult => {
const mergedOptions = {
...defaultPaginationOptions,
...options,
}
const { total, itemsPerPage } = mergedOptions
const nodesCount = Math.ceil((total || 0) / itemsPerPage) || 1

return [calculateNodes(mergedOptions, nodesCount), nodesCount]
}
112 changes: 46 additions & 66 deletions src/packages/pagination/pagination.taro.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { FunctionComponent, useMemo } from 'react'
import React, { FunctionComponent } from 'react'
import classNames from 'classnames'
import { View } from '@tarojs/components'
import { useConfig } from '@/packages/configprovider/index.taro'
import { usePropsValue } from '@/hooks/use-props-value'
import { ComponentDefaults } from '@/utils/typings'
import addColorForHarmony from '@/utils/add-color-for-harmony'
import { WebPaginationProps } from '@/types'
import { PaginationNode, usePagination } from '@/hooks/use-pagination'
import { TaroPaginationProps } from '@/types'

const defaultProps = {
...ComponentDefaults,
@@ -17,9 +18,9 @@ const defaultProps = {
pageSize: 10,
itemSize: 5,
ellipse: false,
} as WebPaginationProps
} as TaroPaginationProps
export const Pagination: FunctionComponent<
Partial<WebPaginationProps> &
Partial<TaroPaginationProps> &
Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>
> = (props) => {
const { locale } = useConfig()
@@ -44,70 +45,50 @@ export const Pagination: FunctionComponent<
}

const classPrefix = 'nut-pagination'
const [currentPage, setCurrentPage] = usePropsValue<number>({
const [current, setCurrent] = usePropsValue<number>({
value,
defaultValue,
finalValue: 1,
onChange,
})

// (total + pageSize) => pageCount 计算页面的数量
const pageCount = useMemo(() => {
const num = Math.ceil(total / pageSize)
return Number.isNaN(num) ? 1 : Math.max(1, num)
}, [total, pageSize])

// (currentPage + itemSize + pageCount) => pages 显示的 item 列表
const pages = useMemo(() => {
const items = [] as Array<any>
let startPage = 1
let endPage = pageCount
const partialShow = pageCount > itemSize
if (partialShow) {
// 选中的 page 放在中间位置
startPage = Math.max(currentPage - Math.floor(itemSize / 2), 1)
endPage = startPage + itemSize - 1
if (endPage > pageCount) {
endPage = pageCount
startPage = endPage - itemSize + 1
}
}
// 遍历生成数组
for (let i = startPage; i <= endPage; i++) {
items.push({ number: i, text: i })
}
// 判断是否有折叠
if (partialShow && itemSize > 0 && ellipse) {
if (startPage > 1) {
items.unshift({ number: startPage - 1, text: '...' })
}
if (endPage < pageCount) {
items.push({ number: endPage + 1, text: '...' })
}
}
return items
}, [currentPage, itemSize, pageCount])
const [pages, pageCount] = usePagination({
total,
ellipse,
current,
displayCount: itemSize,
itemsPerPage: pageSize,
})

const handleSelectPage = (curPage: number) => {
if (curPage > pageCount || curPage < 1) return
setCurrentPage(curPage)
const handleClick = (item: PaginationNode) => {
if (item.selected) return
if (item.number > pageCount || item.number < 1) return
setCurrent(item.number)
}
const prevPage = () => {
const prev = current - 1
prev >= 1 && setCurrent(prev)
}
const nextPage = () => {
const next = current + 1
next <= pageCount && setCurrent(next)
}

return (
<View className={classNames(classPrefix, className)} style={style}>
{(mode === 'multi' || mode === 'simple') && (
<>
<View
className={classNames(
`${classPrefix}-prev`,
mode === 'multi' ? '' : `${classPrefix}-simple-border`,
currentPage === 1 ? `${classPrefix}-prev-disabled` : ''
)}
onClick={(e) => handleSelectPage(currentPage - 1)}
className={classNames({
[`${classPrefix}-prev`]: true,
[`${classPrefix}-simple-border`]: mode !== 'multi',
[`${classPrefix}-prev-disabled`]: current === 1,
})}
onClick={() => prevPage()}
>
{addColorForHarmony(
prev || locale.pagination.prev,
currentPage === 1 ? '#c2c4cc' : '#ff0f23'
current === 1 ? '#c2c4cc' : '#ff0f23'
)}
</View>
{mode === 'multi' && (
@@ -116,16 +97,15 @@ export const Pagination: FunctionComponent<
return (
<View
key={`${index}pagination`}
className={classNames(`${classPrefix}-item`, {
[`${classPrefix}-item-active`]:
item.number === currentPage,
className={classNames({
[`${classPrefix}-item`]: true,
[`${classPrefix}-item-active`]: item.selected,
})}
onClick={(e) => {
item.number !== currentPage &&
handleSelectPage(item.number)
onClick={() => {
handleClick(item)
}}
>
{itemRender ? itemRender(item, currentPage) : item.text}
{itemRender ? itemRender(item, current) : item.text}
</View>
)
})}
@@ -134,27 +114,27 @@ export const Pagination: FunctionComponent<
{mode === 'simple' && (
<View className={`${classPrefix}-contain`}>
<View className={`${classPrefix}-simple`}>
{`${currentPage}/${pageCount}`}
{`${current}/${pageCount}`}
</View>
</View>
)}
<View
className={classNames(
`${classPrefix}-next`,
currentPage >= pageCount ? `${classPrefix}-next-disabled` : ''
)}
onClick={(e) => handleSelectPage(currentPage + 1)}
className={classNames({
[`${classPrefix}-next`]: true,
[`${classPrefix}-next-disabled`]: current >= pageCount,
})}
onClick={() => nextPage()}
>
{addColorForHarmony(
next || locale.pagination.next,
currentPage >= pageCount ? '#c2c4cc' : '#ff0f23'
current >= pageCount ? '#c2c4cc' : '#ff0f23'
)}
</View>
</>
)}
{mode === 'lite' && (
<View className={`${classPrefix}-lite`}>
<View className={`${classPrefix}-lite-active`}>{currentPage}</View>
<View className={`${classPrefix}-lite-active`}>{current}</View>
<View className={`${classPrefix}-lite-spliterator`}>/</View>
<View className={`${classPrefix}-lite-default`}>{pageCount}</View>
</View>
Loading
Loading