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
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-08 12:02:48 +08:00
1 parent 8ff1817845
commit c9b4ceb301
32 files changed
+753 -639

No files matched your search

+116 -89
View File
@@ -14,13 +14,19 @@ export type PaginationProps = {
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]));
}
/** Shared pagination for tables, cards and cursor-backed record lists. */
/**
* 表格、卡片列表与游标列表共用的分页。
* 一页就放得下(只有 1 页、没有更多)时不显示翻页按钮,只留总数;连每页条数也用不上(不超过最小档)时只剩总数;
* 没有任何条目时整块不渲染(空态已经说明)。翻页按钮按容器宽度收起(`@container`):窄卡片里不折成两行。
*/
export function Pagination({
page,
pageSize,
@@ -30,105 +36,126 @@ export function Pagination({
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 (
<nav
aria-label="列表分页"
aria-busy={busy}
className={cn("flex flex-wrap items-center justify-between gap-3 border-t border-border/60 px-4 py-3", className)}
>
<div className="flex flex-wrap items-center gap-3 text-meta text-muted-foreground">
<span aria-live="polite" className="tabular-nums">
{hasMore ? `已加载 ${total} 条` : `共 ${total} 条`} · {from}–{to} 条
</span>
{onPageSizeChange ? (
<div className="flex items-center gap-2">
<span>每页</span>
<Select value={String(pageSize)} disabled={busy} onValueChange={(value) => onPageSizeChange(Number(value))}>
<SelectTrigger size="sm" aria-label="每页条数" className="min-w-24 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>
<div className="flex max-w-full flex-wrap items-center gap-1">
<Button
type="button"
size="sm"
variant="ghost"
aria-label="第一页"
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 sm:inline">上一页</span>
</Button>
{pageNumbers(page, pages).map((n, i) =>
n === "…" ? (
<span key={`gap-${i}`} aria-hidden className="px-1 text-muted-foreground">
…
</span>
) : (
<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
key={n}
type="button"
size="sm"
variant={n === page ? "secondary" : "ghost"}
aria-label={`第 ${n} 页`}
aria-current={n === page ? "page" : undefined}
disabled={busy}
onClick={() => onPageChange(n)}
variant="ghost"
aria-label="第一页"
className="hidden @xl:inline-flex"
disabled={busy || page <= 1}
onClick={() => onPageChange(1)}
>
{n}
<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>
)}
<Button
type="button"
size="sm"
variant="outline"
aria-label="下一页"
disabled={busy || page >= pages}
onClick={() => onPageChange(page + 1)}
>
<span className="hidden sm:inline">下一页</span>
<ChevronRight className="h-4 w-4" />
</Button>
{!hasMore ? (
<Button
type="button"
size="sm"
variant="ghost"
aria-label="最后一页"
disabled={busy || page >= pages}
onClick={() => onPageChange(pages)}
>
<ChevronsRight className="h-4 w-4" />
</Button>
) : null}
</div>
</nav>
</nav>
</div>
);
}