feat: TaskPager with numbered buttons and ResizeObserver-aware width
This commit is contained in:
100
src/renderer/components/tasks/TaskPager.tsx
Normal file
100
src/renderer/components/tasks/TaskPager.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Props {
|
||||
total: number;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onPageSizeChange: (size: number) => void;
|
||||
}
|
||||
|
||||
const PAGE_SIZES = [10, 25, 50, 100];
|
||||
|
||||
function buildWindow(current: number, last: number, max: number): Array<number | 'ellipsis'> {
|
||||
if (last <= 0) return [0];
|
||||
if (last < max) return Array.from({ length: last + 1 }, (_, i) => i);
|
||||
const window: Array<number | 'ellipsis'> = [];
|
||||
const halfMax = Math.floor((max - 2) / 2);
|
||||
let start = Math.max(1, current - halfMax);
|
||||
let end = Math.min(last - 1, current + halfMax);
|
||||
if (current - halfMax < 1) end = Math.min(last - 1, max - 2);
|
||||
if (current + halfMax > last - 1) start = Math.max(1, last - (max - 2));
|
||||
window.push(0);
|
||||
if (start > 1) window.push('ellipsis');
|
||||
for (let i = start; i <= end; i++) window.push(i);
|
||||
if (end < last - 1) window.push('ellipsis');
|
||||
window.push(last);
|
||||
return window;
|
||||
}
|
||||
|
||||
export function TaskPager({ total, pageIndex, pageSize, onPageChange, onPageSizeChange }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [maxButtons, setMaxButtons] = useState(7);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(([entry]) => {
|
||||
const w = entry.contentRect.width;
|
||||
setMaxButtons(w < 480 ? 3 : w < 640 ? 5 : 7);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
const lastPage = Math.max(0, Math.ceil(total / pageSize) - 1);
|
||||
const start = total === 0 ? 0 : pageIndex * pageSize + 1;
|
||||
const end = Math.min(total, (pageIndex + 1) * pageSize);
|
||||
const window = buildWindow(pageIndex, lastPage, maxButtons);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="rounded-lg border border-border/50 bg-card/65 backdrop-blur-xl shadow-sm flex items-center justify-between px-4 py-2 gap-3 flex-wrap"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Trans
|
||||
i18nKey="tasks.showingNofM"
|
||||
values={{ start, end, total }}
|
||||
components={{ b: <span className="font-medium text-foreground" /> }}
|
||||
/>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">{t('tasks.rowsPerPage')}</span>
|
||||
<Select value={String(pageSize)} onValueChange={(v) => onPageSizeChange(Number(v))}>
|
||||
<SelectTrigger className="h-7 w-[68px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{PAGE_SIZES.map((s) => <SelectItem key={s} value={String(s)}>{s}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" disabled={pageIndex === 0} onClick={() => onPageChange(pageIndex - 1)}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{window.map((p, i) =>
|
||||
p === 'ellipsis' ? (
|
||||
<span key={`e${i}`} className="px-1 text-muted-foreground">…</span>
|
||||
) : (
|
||||
<Button
|
||||
key={p}
|
||||
variant={p === pageIndex ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className={cn('h-7 min-w-7 px-2 text-xs')}
|
||||
onClick={() => onPageChange(p)}
|
||||
>
|
||||
{p + 1}
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" disabled={pageIndex >= lastPage} onClick={() => onPageChange(pageIndex + 1)}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user