feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
apps/web/components/Pagination.tsx
Normal file
95
apps/web/components/Pagination.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
|
||||
interface PaginationProps {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
onChange: (page: number) => void;
|
||||
onPageSizeChange: (size: number) => void;
|
||||
}
|
||||
|
||||
export function Pagination({ total, page, pageSize, onChange, onPageSizeChange }: PaginationProps) {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
if (total === 0) return null;
|
||||
|
||||
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 start = Math.max(2, page - 1);
|
||||
const end = Math.min(totalPages - 1, page + 1);
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
if (page < totalPages - 2) pages.push('...');
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-1 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[12px] text-[var(--ink-muted)]">共 {total} 条</span>
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||
className="h-7 rounded-md border border-[var(--line)] bg-[var(--bg-card)] px-1.5 text-[12px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||
<option key={size} value={size}>{size} 条/页</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
disabled={page <= 1}
|
||||
onClick={() => onChange(page - 1)}
|
||||
className="h-7 w-7 flex items-center justify-center rounded-md border border-[var(--line)] text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)] disabled:opacity-40 disabled:pointer-events-none"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{pages.map((p, idx) =>
|
||||
p === '...' ? (
|
||||
<span key={`dots-${idx}`} className="w-7 text-center text-[12px] text-[var(--ink-muted)]">...</span>
|
||||
) : (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => onChange(p as number)}
|
||||
className={`h-7 w-7 flex items-center justify-center rounded-md text-[12px] font-medium transition-colors ${page === p ? 'bg-[var(--accent)] text-white' : 'border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]'}`}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => onChange(page + 1)}
|
||||
className="h-7 w-7 flex items-center justify-center rounded-md border border-[var(--line)] text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)] disabled:opacity-40 disabled:pointer-events-none"
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePagination<T>(items: T[], defaultPageSize: number = 20) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(defaultPageSize);
|
||||
|
||||
const total = items.length;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const safePage = Math.min(page, totalPages);
|
||||
const paged = items.slice((safePage - 1) * pageSize, safePage * pageSize);
|
||||
|
||||
const handlePageSizeChange = (newSize: number) => {
|
||||
setPageSize(newSize);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return { paged, page: safePage, setPage, total, pageSize, setPageSize: handlePageSizeChange };
|
||||
}
|
||||
Reference in New Issue
Block a user