Files
UHD_Media_Tool/apps/web/app/components/ui/pagination.tsx
T
rootandClaude Opus 5.5 c9b4ceb301 fix(web): 控制台界面细节与 uhd_web 控件规范对齐
- 顶栏搜索移进右侧玻璃胶囊;输入框改淡玻璃(边线 0.46 → 0.16,悬停加深,聚焦 / 出错保留光晕)
- 新增 SegmentedControl(滑动 seg-thumb),替换站点、下载、订阅、文件、智能体列表及对话框里的实心选中块;Tabs 改为同一枚滑块
- 开关型按钮统一 outline + aria-pressed 选中外观;页头 / 工具栏次操作描边、图标钮描边圆;文件排序改 Select;下载总速度加标签
- 分页一页放得下时只留总数、空列表不渲染、按容器宽度收起按钮
- 运行状态存储区重排;智能体列表页与会话页共用外壳路由,标题与左栏不再重挂载
- 站点卡片「⋯」不再单独换行;设置导航、状态计数、多选条件改玻璃选中态

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-08 12:02:48 +08:00

162 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
import { PAGE_SIZES } from "~/components/console/use-list-pagination.ts";
import { Button } from "~/components/ui/button.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
import { cn } from "~/lib/utils.ts";
export type PaginationProps = {
page: number;
pageSize: number;
/** With hasMore, total is the loaded count, not a complete total. */
total: number;
hasMore?: boolean;
busy?: boolean;
onPageChange: (page: number) => void;
onPageSizeChange?: (size: number) => void;
className?: string;
/** 一页放得下时整块不显示(卡片网格下面单独一行「共 N 条」显得悬空) */
hideSingle?: boolean;
};
export function pageNumbers(page: number, pages: number): (number | "…")[] {
const values = new Set([1, pages, page - 1, page, page + 1]);
const valid = [...values].filter((n) => n >= 1 && n <= pages).sort((a, b) => a - b);
return valid.flatMap((n, i): (number | "…")[] => (i && n - valid[i - 1] > 1 ? ["…", n] : [n]));
}
/**
* 表格、卡片列表与游标列表共用的分页。
* 一页就放得下(只有 1 页、没有更多)时不显示翻页按钮,只留总数;连每页条数也用不上(不超过最小档)时只剩总数;
* 没有任何条目时整块不渲染(空态已经说明)。翻页按钮按容器宽度收起(`@container`):窄卡片里不折成两行。
*/
export function Pagination({
page,
pageSize,
total,
hasMore = false,
busy = false,
onPageChange,
onPageSizeChange,
className,
hideSingle = false,
}: PaginationProps) {
const loadedPages = Math.max(1, Math.ceil(total / pageSize));
const pages = loadedPages + (hasMore ? 1 : 0);
const from = total ? (page - 1) * pageSize + 1 : 0;
const to = Math.min(page * pageSize, total);
if (!total && !hasMore) return null;
const single = pages <= 1;
const showSize = !!onPageSizeChange && total > PAGE_SIZES[0];
if (single && hideSingle && !showSize) return null;
return (
<div className={cn("@container border-t border-border/60", className)}>
<nav
aria-label="列表分页"
aria-busy={busy}
className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 px-4 py-2.5 @lg:flex-nowrap"
>
<div className="flex min-w-0 items-center gap-3 text-meta whitespace-nowrap text-muted-foreground">
<span aria-live="polite" className="tabular-nums">
{hasMore ? `已加载 ${total} 条` : `共 ${total} 条`}
{single ? null : (
<span className="hidden @md:inline">
{" "}
· {from}–{to} 条
</span>
)}
</span>
{showSize ? (
<div className="flex items-center gap-2">
<span className="hidden @sm:inline">每页</span>
<Select
value={String(pageSize)}
disabled={busy}
onValueChange={(value) => onPageSizeChange?.(Number(value))}
>
<SelectTrigger size="sm" aria-label="每页条数" className="min-w-22 text-caption text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" align="start" collisionPadding={16}>
{PAGE_SIZES.map((size) => (
<SelectItem key={size} value={String(size)}>
{size} 条
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
</div>
{single ? null : (
<div className="ml-auto flex shrink-0 items-center gap-1">
<Button
type="button"
size="sm"
variant="ghost"
aria-label="第一页"
className="hidden @xl:inline-flex"
disabled={busy || page <= 1}
onClick={() => onPageChange(1)}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
type="button"
size="sm"
variant="outline"
aria-label="上一页"
disabled={busy || page <= 1}
onClick={() => onPageChange(page - 1)}
>
<ChevronLeft className="h-4 w-4" />
<span className="hidden @2xl:inline">上一页</span>
</Button>
{pageNumbers(page, pages).map((n, i) =>
n === "…" ? (
<span key={`gap-${i}`} aria-hidden className="px-1 text-muted-foreground">
…
</span>
) : (
<Button
key={n}
type="button"
size="sm"
variant={n === page ? "outline" : "ghost"}
aria-label={`第 ${n} 页`}
aria-current={n === page ? "page" : undefined}
disabled={busy}
onClick={() => onPageChange(n)}
>
{n}
</Button>
),
)}
<Button
type="button"
size="sm"
variant="outline"
aria-label="下一页"
disabled={busy || page >= pages}
onClick={() => onPageChange(page + 1)}
>
<span className="hidden @2xl:inline">下一页</span>
<ChevronRight className="h-4 w-4" />
</Button>
{!hasMore ? (
<Button
type="button"
size="sm"
variant="ghost"
aria-label="最后一页"
className="hidden @xl:inline-flex"
disabled={busy || page >= pages}
onClick={() => onPageChange(pages)}
>
<ChevronsRight className="h-4 w-4" />
</Button>
) : null}
</div>
)}
</nav>
</div>
);
}