Files
ttstd-infinite-canvas/web/src/components/layout/workspace-page.tsx

88 lines
5.3 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 { Button, Select } from "antd";
import { ChevronLeft, ChevronRight, RotateCcw } from "lucide-react";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export function WorkspacePage({ children, className, grid = false, fluid = false }: { children: ReactNode; className?: string; grid?: boolean; fluid?: boolean }) {
return (
<main className={cn("app-user-content app-workspace-scroll h-full overflow-y-auto text-foreground", grid && "app-workspace-grid", className)}>
<div className={fluid ? "h-full w-full" : "w-full px-3 py-3 sm:px-4 sm:py-4 xl:px-5"}>{children}</div>
</main>
);
}
export function PageHeader({ title, description, meta, actions }: { title: string; description?: string; meta?: ReactNode; actions?: ReactNode }) {
return (
<header className="app-page-header flex min-h-14 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-center gap-3">
<div className="min-w-0">
<div className="flex min-w-0 flex-wrap items-center gap-2.5">
<h1 className="app-page-header-title truncate font-semibold leading-7">{title}</h1>
{meta}
</div>
{description ? <p className="mt-1 text-xs leading-5 text-foreground/58">{description}</p> : null}
</div>
</div>
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
</header>
);
}
export function ListToolbar({ children, trailing, active, onReset, className }: { children: ReactNode; trailing?: ReactNode; active?: boolean; onReset?: () => void; className?: string }) {
return (
<div className={cn("mt-3 flex min-h-12 flex-col gap-2 pb-3 lg:flex-row lg:items-center lg:justify-between", className)}>
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2.5">{children}</div>
<div className="flex shrink-0 flex-wrap items-center gap-2">
{active && onReset ? <Button type="text" icon={<RotateCcw className="size-3.5" />} onClick={onReset}>重置</Button> : null}
{trailing}
</div>
</div>
);
}
export function TableSurface({ children, className }: { children: ReactNode; className?: string }) {
return <div className={cn("app-table-surface mt-4 min-w-0 overflow-hidden rounded-lg bg-surface", className)}>{children}</div>;
}
export function CollectionGrid({ children, className }: { children: ReactNode; className?: string }) {
return <div className={cn("mt-4 grid grid-cols-1 gap-4 sm:grid-cols-[repeat(auto-fill,minmax(248px,1fr))]", className)}>{children}</div>;
}
/* 自研轻量分页:页码胶囊 + 省略号 + 每页条数 + 总数,替代 AntD Pagination(无 AntD 残留样式,与工具栏容器同语言)。 */
function pageItems(current: number, pages: number): (number | "…")[] {
if (pages <= 7) return Array.from({ length: pages }, (_, i) => i + 1);
if (current <= 4) return [1, 2, 3, 4, 5, "…", pages];
if (current >= pages - 3) return [1, "…", pages - 4, pages - 3, pages - 2, pages - 1, pages];
return [1, "…", current - 1, current, current + 1, "…", pages];
}
export function PaginationBar({ current, pageSize, total, onChange, pageSizeOptions = [20, 50, 100] }: { current: number; pageSize: number; total: number; onChange: (page: number, pageSize: number) => void; pageSizeOptions?: number[] }) {
if (total <= pageSize && current === 1) return null;
const pages = Math.max(1, Math.ceil(total / pageSize));
const start = (current - 1) * pageSize + 1;
const end = Math.min(total, current * pageSize);
const items = pageItems(current, pages);
return (
<div className="app-pagination-bar mt-4 flex min-h-10 min-w-0 items-center justify-end gap-2 px-2 py-1.5">
<span className="app-pagination-total">{start}-{end} / 共 {total} 条</span>
<Select
size="small"
value={pageSize}
className="app-pagination-size"
options={pageSizeOptions.map((size) => ({ value: size, label: `${size} 条/页` }))}
onChange={(value) => onChange(1, Number(value))}
/>
<div className="app-pagination-pages" role="navigation" aria-label="分页">
<button type="button" className="app-pagination-btn app-pagination-prev" disabled={current <= 1} aria-label="上一页" onClick={() => onChange(current - 1, pageSize)}><ChevronLeft className="size-4" /></button>
{items.map((item) => item === "…" ? (
<span key={`ellipsis-${items.indexOf(item)}`} className="app-pagination-ellipsis">…</span>
) : (
<button key={item} type="button" className={`app-pagination-btn${item === current ? " is-active" : ""}`} aria-current={item === current ? "page" : undefined} onClick={() => onChange(item, pageSize)}>{item}</button>
))}
<button type="button" className="app-pagination-btn app-pagination-next" disabled={current >= pages} aria-label="下一页" onClick={() => onChange(current + 1, pageSize)}><ChevronRight className="size-4" /></button>
</div>
</div>
);
}