feat: 分页组件、Dashboard待办图标、归档与工资条解耦、总览数据优化
- 新增公用 Pagination 组件,Roster/Money/Dashboard 列表加分页 - Dashboard 待办按类型显示不同图标(合同/薪资/解聘/月度) - 待办分为「风险提醒」「月度任务」两个顶层 tab - 归档与工资条生成解耦:归档只锁定批次,工资条单独生成 - 工资条管理新增「从批次汇总生成」按钮 - Dashboard 总览优先从已归档批次 BatchEntry 汇总数据 - 新增工资条生成待办提醒,生成后自动标记完成 - 修复高风险统计只含 CONTRACT/TERMINATION 类型 - 修复月度任务去重逻辑覆盖 SALARY 类型
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import clsx from 'clsx'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
|
||||
interface PaginationProps {
|
||||
page: number // 当前页(1-based)
|
||||
pageSize: number // 每页条数
|
||||
total: number // 总条数
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange?: (size: number) => void
|
||||
pageSizeOptions?: number[]
|
||||
}
|
||||
|
||||
export default function Pagination({
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
pageSizeOptions = [10, 20, 50],
|
||||
}: PaginationProps) {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
||||
const start = total === 0 ? 0 : (page - 1) * pageSize + 1
|
||||
const end = Math.min(page * pageSize, total)
|
||||
|
||||
// 生成页码按钮(最多显示 7 个)
|
||||
const pages: (number | '...')[] = []
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i)
|
||||
} else {
|
||||
pages.push(1)
|
||||
if (page > 3) pages.push('...')
|
||||
const s = Math.max(2, page - 1)
|
||||
const e = Math.min(totalPages - 1, page + 1)
|
||||
for (let i = s; i <= e; i++) pages.push(i)
|
||||
if (page < totalPages - 2) pages.push('...')
|
||||
pages.push(totalPages)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
{/* 左侧:条数信息 + 每页条数选择 */}
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||
<span>共 {total} 条</span>
|
||||
{onPageSizeChange && (
|
||||
<select
|
||||
className="border rounded px-1 py-0.5 text-xs text-gray-600 focus:outline-none focus:border-primary"
|
||||
value={pageSize}
|
||||
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||
>
|
||||
{pageSizeOptions.map((n) => (
|
||||
<option key={n} value={n}>{n} 条/页</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<span>第 {start}-{end} 条</span>
|
||||
</div>
|
||||
|
||||
{/* 右侧:页码导航 */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
className="p-1 rounded text-gray-400 hover:text-gray-700 hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
disabled={page <= 1}
|
||||
onClick={() => onPageChange(page - 1)}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
{pages.map((p, i) =>
|
||||
p === '...' ? (
|
||||
<span key={`ellipsis-${i}`} className="px-2 text-gray-400 text-xs">…</span>
|
||||
) : (
|
||||
<button
|
||||
key={p}
|
||||
className={clsx(
|
||||
'min-w-[28px] h-7 rounded text-xs font-medium transition-colors',
|
||||
p === page
|
||||
? 'bg-primary text-white'
|
||||
: 'text-gray-600 hover:bg-gray-100',
|
||||
)}
|
||||
onClick={() => onPageChange(p)}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
<button
|
||||
className="p-1 rounded text-gray-400 hover:text-gray-700 hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user