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:
1 parent
8ff1817845
commit
c9b4ceb301
32 files changed
+753
-639
No files matched your search
@@ -1,69 +1,47 @@
|
||||
import { ArrowLeft, PanelRightOpen } from "lucide-react";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import { Link, useNavigate, useOutletContext, useParams } from "react-router";
|
||||
import { agentMockEnabled } from "~/api/agent.ts";
|
||||
import {
|
||||
agentListHref,
|
||||
agentRunHref,
|
||||
pageListMemory,
|
||||
usePageListGroup,
|
||||
useRunGroup,
|
||||
} from "~/components/agent/agent-nav.ts";
|
||||
import { agentListHref } from "~/components/agent/agent-nav.ts";
|
||||
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||
import { agentReturnPath, showAgentRunInDrawer } from "~/components/agent/agent-store.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import type { AgentOutletContext } from "./agent.tsx";
|
||||
|
||||
// 会话页(AGUI-3):左栏会话列表(xl 以上)· 会话列占满其余宽度(顶部概况条、底部输入区)。
|
||||
// 不再有页头目标描述和右侧栏;步骤、子会话、预算与模型在概况条里。
|
||||
// 会话页的右栏(AGUI-3):会话列占满(顶部概况条、底部输入区);标题与左栏会话列表在外壳 agent.tsx,切换会话不重挂载。
|
||||
// 步骤、子会话、预算与模型在概况条里。
|
||||
export default function AgentRunPage() {
|
||||
const { runId = "" } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const mock = agentMockEnabled();
|
||||
// 左栏分栏跟随当前会话(巡检会话及其子会话在「自动巡检」),也可以手动切换(记在 URL)
|
||||
const [group, setGroup] = usePageListGroup(useRunGroup(runId));
|
||||
const { group } = useOutletContext<AgentOutletContext>();
|
||||
return (
|
||||
<>
|
||||
<h1 className="sr-only">智能体会话</h1>
|
||||
<div data-agent-page className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,17rem)_minmax(0,1fr)]">
|
||||
<aside className="pg-slab hidden rounded-xl p-3 xl:block xl:h-[calc(100dvh-7rem)] xl:overflow-hidden">
|
||||
<AgentSessionList
|
||||
activeId={runId}
|
||||
hrefFor={(id) => agentRunHref(id, mock)}
|
||||
group={group}
|
||||
onGroupChange={setGroup}
|
||||
memory={pageListMemory}
|
||||
className="h-full"
|
||||
/>
|
||||
</aside>
|
||||
<div className="pg-slab flex h-[calc(100dvh-7rem)] min-h-[28rem] flex-col gap-2 rounded-xl p-3 md:p-4">
|
||||
<Button asChild variant="ghost" size="sm" className="-mt-1 -ml-2 self-start text-muted-foreground xl:hidden">
|
||||
<Link to={agentListHref(group, mock)}>
|
||||
<ArrowLeft />
|
||||
全部会话
|
||||
</Link>
|
||||
<div className="pg-slab flex h-[calc(100dvh-10.25rem)] min-h-[28rem] flex-col gap-2 rounded-2xl p-3 md:p-4 lg:h-[calc(100dvh-11.25rem)]">
|
||||
<Button asChild variant="ghost" size="sm" className="-mt-1 -ml-2 self-start text-muted-foreground lg:hidden">
|
||||
<Link to={agentListHref(group, mock)}>
|
||||
<ArrowLeft />
|
||||
全部会话
|
||||
</Link>
|
||||
</Button>
|
||||
<AgentRunPanel
|
||||
key={runId}
|
||||
runId={runId}
|
||||
className="min-h-0 flex-1"
|
||||
actions={
|
||||
// 整页 → 抽屉:回到进智能体页面之前的页面,会话在抽屉里接着看
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label="在侧边打开"
|
||||
title="在侧边打开,回到之前的页面"
|
||||
onClick={() => {
|
||||
showAgentRunInDrawer(runId);
|
||||
navigate(agentReturnPath() ?? "/console");
|
||||
}}
|
||||
>
|
||||
<PanelRightOpen />
|
||||
</Button>
|
||||
<AgentRunPanel
|
||||
key={runId}
|
||||
runId={runId}
|
||||
className="min-h-0 flex-1"
|
||||
actions={
|
||||
// 整页 → 抽屉:回到进智能体页面之前的页面,会话在抽屉里接着看
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="在侧边打开"
|
||||
title="在侧边打开,回到之前的页面"
|
||||
onClick={() => {
|
||||
showAgentRunInDrawer(runId);
|
||||
navigate(agentReturnPath() ?? "/console");
|
||||
}}
|
||||
>
|
||||
<PanelRightOpen />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { agentMockEnabled, useCreateAgentRun } from "~/api/agent.ts";
|
||||
import { agentRunHref, DRAFT_PAGE_GOAL, writeDraft } from "~/components/agent/agent-nav.ts";
|
||||
import { GoalComposer } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { quickGoalsFor } from "~/components/agent/quick-goals.ts";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
|
||||
// 智能体列表页的右栏:新对话输入框 + 3 个快捷目标(标题与左栏会话列表在外壳 agent.tsx)
|
||||
export default function AgentIndexPage() {
|
||||
const create = useCreateAgentRun();
|
||||
const navigate = useNavigate();
|
||||
const mock = agentMockEnabled();
|
||||
return (
|
||||
<div className="pg-slab mx-auto w-full max-w-[88ch] rounded-2xl p-4">
|
||||
<GoalComposer
|
||||
pending={create.isPending}
|
||||
suggestions={quickGoalsFor("/console/agent")}
|
||||
draftKey={DRAFT_PAGE_GOAL}
|
||||
onSubmit={(goal, opts) =>
|
||||
create.mutate(
|
||||
{ goal, ...opts },
|
||||
{
|
||||
onSuccess: (run) => {
|
||||
writeDraft(DRAFT_PAGE_GOAL, "");
|
||||
navigate(agentRunHref(run.id, mock));
|
||||
},
|
||||
onError: (e) => toast.error(errorMessage(e)),
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,27 @@
|
||||
import { FlaskConical } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { agentMockEnabled, useCreateAgentRun } from "~/api/agent.ts";
|
||||
import {
|
||||
agentRunHref,
|
||||
DRAFT_PAGE_GOAL,
|
||||
pageListMemory,
|
||||
usePageListGroup,
|
||||
writeDraft,
|
||||
} from "~/components/agent/agent-nav.ts";
|
||||
import { GoalComposer } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { Outlet, useParams } from "react-router";
|
||||
import { agentMockEnabled } from "~/api/agent.ts";
|
||||
import { agentRunHref, pageListMemory, usePageListGroup, useRunGroup } from "~/components/agent/agent-nav.ts";
|
||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||
import { quickGoalsFor } from "~/components/agent/quick-goals.ts";
|
||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
// 智能体入口页(AGUI-3):左栏会话列表(等你处理置顶),右边新对话输入框 + 3 个快捷目标。
|
||||
export default function AgentPage() {
|
||||
const create = useCreateAgentRun();
|
||||
const navigate = useNavigate();
|
||||
/**
|
||||
* 智能体页的外壳(AGUI-3):列表页与会话页共用。标题、演示提示与左栏会话列表在这里,只挂载一次;
|
||||
* 右栏是子路由(新对话输入框 / 某个会话)。切换会话、回到列表都不重挂载左栏,也不重播入场动画,
|
||||
* 标题与两栏的位置不跳。
|
||||
*/
|
||||
export default function AgentLayout() {
|
||||
const { runId } = useParams();
|
||||
const mock = agentMockEnabled();
|
||||
const [group, setGroup] = usePageListGroup();
|
||||
const runGroupOf = useRunGroup(runId);
|
||||
// 列表页没有 tab 就是「对话」;会话页的左栏分栏跟随当前会话(巡检会话及其子会话在「自动巡检」),也可以手动切换(记在 URL)
|
||||
const [group, setGroup] = usePageListGroup(runId ? runGroupOf : undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="智能体" />
|
||||
<PageHeader title="智能体" className="mb-4 md:mb-6" />
|
||||
{mock ? (
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-caption text-warning">
|
||||
<FlaskConical className="h-4 w-4 shrink-0" />
|
||||
@@ -37,37 +33,31 @@ export default function AgentPage() {
|
||||
) : null}
|
||||
<div
|
||||
data-agent-page
|
||||
className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] lg:items-start"
|
||||
// 智能体页没有右下角悬浮按钮,收回主内容区为它留的底部空白,两栏正好占满一屏
|
||||
className="-mb-16 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,19rem)_minmax(0,1fr)] lg:items-start"
|
||||
>
|
||||
<div className="pg-slab order-2 rounded-xl p-3 lg:order-1 lg:h-[calc(100dvh-12rem)] lg:overflow-hidden">
|
||||
{/* 窄屏:列表页把会话列表排在输入框下面;会话页不显示列表(顶部有「全部会话」返回) */}
|
||||
<aside
|
||||
className={cn(
|
||||
"pg-slab order-2 rounded-2xl p-3 lg:order-1 lg:block lg:h-[calc(100dvh-11.25rem)] lg:overflow-hidden",
|
||||
runId && "hidden",
|
||||
)}
|
||||
>
|
||||
<AgentSessionList
|
||||
activeId={runId}
|
||||
hrefFor={(id) => agentRunHref(id, mock)}
|
||||
group={group}
|
||||
onGroupChange={setGroup}
|
||||
memory={pageListMemory}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="pg-slab order-1 mx-auto w-full max-w-[88ch] rounded-xl p-4 lg:order-2">
|
||||
<GoalComposer
|
||||
pending={create.isPending}
|
||||
suggestions={quickGoalsFor("/console/agent")}
|
||||
draftKey={DRAFT_PAGE_GOAL}
|
||||
onSubmit={(goal, opts) =>
|
||||
create.mutate(
|
||||
{ goal, ...opts },
|
||||
{
|
||||
onSuccess: (run) => {
|
||||
writeDraft(DRAFT_PAGE_GOAL, "");
|
||||
navigate(agentRunHref(run.id, mock));
|
||||
},
|
||||
onError: (e) => toast.error(errorMessage(e)),
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
</aside>
|
||||
<div className="order-1 min-w-0 lg:order-2">
|
||||
<Outlet context={{ group } satisfies AgentOutletContext} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export type AgentOutletContext = { group: ReturnType<typeof usePageListGroup>[0] };
|
||||
@@ -97,6 +97,8 @@ import { type OrganizeSource, OrganizeWizard } from "~/components/organize-wizar
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { SegmentedControl } from "~/components/ui/segmented.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
@@ -797,23 +799,14 @@ export default function FilesPage() {
|
||||
|
||||
// ── 工具栏 ──
|
||||
const chips = (
|
||||
<div className="flex gap-1 overflow-x-auto scroll-bleed">
|
||||
{FILTER_CHIPS.map((c) => (
|
||||
<button
|
||||
key={c.value}
|
||||
type="button"
|
||||
aria-pressed={filter === c.value}
|
||||
onClick={() => patch({ f: c.value || null })}
|
||||
className={cn(
|
||||
"shrink-0 rounded-full border px-2.5 py-1 text-meta whitespace-nowrap transition-colors duration-micro",
|
||||
filter === c.value
|
||||
? "pill-selected text-foreground"
|
||||
: "border-border text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{c.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="min-w-0 overflow-x-auto scroll-bleed">
|
||||
<SegmentedControl
|
||||
label="筛选"
|
||||
size="xs"
|
||||
value={filter || "all"}
|
||||
onChange={(v) => patch({ f: v === "all" ? null : v })}
|
||||
options={FILTER_CHIPS.map((c) => ({ value: c.value || "all", label: c.label }))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const searchBox = (
|
||||
@@ -838,21 +831,21 @@ export default function FilesPage() {
|
||||
</div>
|
||||
);
|
||||
const sortControls = (
|
||||
<div className="flex items-center gap-1">
|
||||
<select
|
||||
aria-label="排序"
|
||||
value={sort}
|
||||
onChange={(e) => patch({ sort: e.target.value === "name" ? null : e.target.value })}
|
||||
className="field-glass h-8 rounded-xl px-2 text-caption"
|
||||
>
|
||||
{SORT_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
按{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={sort} onValueChange={(v) => patch({ sort: v === "name" ? null : v })}>
|
||||
<SelectTrigger size="sm" aria-label="排序" className="min-w-28 text-caption">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" align="end">
|
||||
{SORT_OPTIONS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
按{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label={order === "asc" ? "升序,切换为降序" : "降序,切换为升序"}
|
||||
title={order === "asc" ? "升序" : "降序"}
|
||||
@@ -861,13 +854,12 @@ export default function FilesPage() {
|
||||
{order === "asc" ? <ArrowDownAZ /> : <ArrowUpAZ />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-pressed={hidden}
|
||||
aria-label={hidden ? "不显示隐藏文件" : "显示隐藏文件"}
|
||||
title={hidden ? "正在显示隐藏文件" : "显示隐藏文件"}
|
||||
onClick={() => patch({ hidden: hidden ? null : "1" })}
|
||||
className={cn(hidden && "bg-foreground/[0.08] text-foreground")}
|
||||
>
|
||||
{hidden ? <Eye /> : <EyeOff />}
|
||||
</Button>
|
||||
@@ -1007,7 +999,7 @@ export default function FilesPage() {
|
||||
<span className="flex-1" />
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label={searchOpen ? "收起搜索" : "搜索"}
|
||||
onClick={() => {
|
||||
@@ -1018,7 +1010,7 @@ export default function FilesPage() {
|
||||
{searchOpen ? <X /> : <Search />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label="更多"
|
||||
onClick={(ev) => openMenuAt(ev.currentTarget, mobileMore())}
|
||||
@@ -1032,7 +1024,7 @@ export default function FilesPage() {
|
||||
<div className="space-y-2 border-b border-border px-3 py-2.5">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label="上一级"
|
||||
title="上一级(Alt+↑)"
|
||||
@@ -1053,7 +1045,7 @@ export default function FilesPage() {
|
||||
<span className="flex-1" />
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label="刷新"
|
||||
title="刷新"
|
||||
@@ -1067,7 +1059,7 @@ export default function FilesPage() {
|
||||
<RotateCw className={cn(list.isFetching && !list.isFetchingNextPage && "animate-spin")} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label="快捷键"
|
||||
title="快捷键(?)"
|
||||
|
||||
@@ -234,7 +234,7 @@ function ConsoleShell({ session }: { session: SessionInfo }) {
|
||||
)}
|
||||
>
|
||||
{/* 顶栏不铺磨砂(磨砂栏会成为背景根,里面的玻璃糊不到页面):页面底色渐隐托住标题,
|
||||
右侧通知、智能体、日夜切换与账户收进一枚玻璃胶囊 */}
|
||||
右侧搜索、通知、智能体、日夜切换与账户收进一枚玻璃胶囊 */}
|
||||
<header className="sticky top-0 z-20 flex h-14 items-center bg-gradient-to-b from-background/90 via-background/60 to-transparent px-4 md:px-6">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
@@ -255,9 +255,9 @@ function ConsoleShell({ session }: { session: SessionInfo }) {
|
||||
<span className="truncate text-sm font-semibold text-foreground">{activeLabel}</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<SearchTrigger />
|
||||
<LiquidSurface tier="regular" radius={20} noRefraction style={{ width: "auto", height: "auto" }}>
|
||||
<div className="flex items-center gap-0.5 p-1">
|
||||
<SearchTrigger className="mr-1" />
|
||||
<InboxBell bare />
|
||||
{/* 桌面有侧栏主入口与右下角悬浮按钮,顶栏的智能体按钮只在窄屏保留 */}
|
||||
<AgentDrawerTrigger className="xl:hidden" />
|
||||
|
||||
@@ -179,14 +179,13 @@ function LogsView({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setLive((v) => !v)}
|
||||
aria-pressed={live}
|
||||
aria-label={live ? "暂停实时" : "继续实时"}
|
||||
>
|
||||
{live ? <Pause /> : <Play />}
|
||||
{live ? "暂停" : "继续"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
stream.clear();
|
||||
@@ -243,7 +242,7 @@ function LogsView({
|
||||
allLabel="不限"
|
||||
/>
|
||||
<Button
|
||||
variant={filters.system === "1" ? "secondary" : "ghost"}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-pressed={filters.system === "1"}
|
||||
title="没有业务对象的任务、例行定时任务的成功运行"
|
||||
|
||||
@@ -326,9 +326,10 @@ function StatsLine({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStatus("")}
|
||||
aria-pressed={!status}
|
||||
className={cn(
|
||||
"rounded-md px-1.5 py-0.5 hover:text-foreground",
|
||||
!status && "bg-foreground/[0.06] text-foreground",
|
||||
"cursor-pointer rounded-full border border-transparent px-2.5 py-0.5 hover:text-foreground",
|
||||
!status && "pill-selected text-foreground",
|
||||
)}
|
||||
>
|
||||
共 {s.total} 条
|
||||
@@ -339,8 +340,8 @@ function StatsLine({
|
||||
aria-pressed={attention}
|
||||
onClick={() => onAttention(!attention)}
|
||||
className={cn(
|
||||
"rounded-md px-1.5 py-0.5 text-warning hover:text-foreground",
|
||||
attention && "bg-foreground/[0.06]",
|
||||
"cursor-pointer rounded-full border border-transparent px-2.5 py-0.5 text-warning hover:text-foreground",
|
||||
attention && "pill-selected",
|
||||
)}
|
||||
>
|
||||
需要处理 {s.attention}
|
||||
@@ -355,9 +356,9 @@ function StatsLine({
|
||||
aria-pressed={status === p.key}
|
||||
onClick={() => onStatus(status === p.key ? "" : p.key)}
|
||||
className={cn(
|
||||
"rounded-md px-1.5 py-0.5 hover:text-foreground",
|
||||
"cursor-pointer rounded-full border border-transparent px-2.5 py-0.5 hover:text-foreground",
|
||||
p.tone,
|
||||
status === p.key && "bg-foreground/[0.06]",
|
||||
status === p.key && "pill-selected",
|
||||
)}
|
||||
>
|
||||
{p.label} {p.n}
|
||||
@@ -668,8 +669,9 @@ export default function OrganizePage() {
|
||||
<>
|
||||
<OrganizeQueueButton open={queueOpen} onOpenChange={setQueueOpen} />
|
||||
<Button
|
||||
variant={selecting ? "secondary" : "ghost"}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-pressed={selecting}
|
||||
onClick={() => (selecting ? exitSelect() : setSelecting(true))}
|
||||
>
|
||||
{selecting ? <X /> : <SquareCheck />}
|
||||
@@ -687,7 +689,7 @@ export default function OrganizePage() {
|
||||
search={{ value: qInput, onChange: setQInput, placeholder: "作品、路径或 hash,支持 * ?" }}
|
||||
actions={
|
||||
<Button
|
||||
variant={grouped ? "secondary" : "ghost"}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-pressed={grouped}
|
||||
onClick={() => setGrouping(grouped ? "none" : "entity")}
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function SettingsPage() {
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors duration-micro",
|
||||
active
|
||||
? "bg-foreground/[0.07] font-medium text-foreground"
|
||||
? "seg-thumb font-medium text-foreground"
|
||||
: "text-muted-foreground hover:bg-foreground/[0.04] hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SiteCard, SiteTable } from "~/components/sites/site-card.tsx";
|
||||
import { type SiteDialog, SiteDialogHost, useSiteChecks } from "~/components/sites/site-dialogs.tsx";
|
||||
import { needsAttention, type SortKey, sortSites } from "~/components/sites/site-fields.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { SegmentedControl } from "~/components/ui/segmented.tsx";
|
||||
|
||||
// 获取 → 站点:站点、凭证、健康、账号数据、RSS 的唯一入口(docs/design/site-center.md §4);
|
||||
// 按类型分组显示:PT 站 / 公开 BT 站(旧的自定义 RSS 源有的话单列);?new=<类型> 直接打开添加。
|
||||
@@ -143,13 +143,13 @@ export default function SitesPage() {
|
||||
{all.length ? (
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Segmented
|
||||
<SegmentedControl
|
||||
label="筛选"
|
||||
value={filter}
|
||||
onChange={(v) => setParam("filter", v === "all" ? null : v)}
|
||||
options={FILTERS.map((f) => ({
|
||||
...f,
|
||||
label: f.value === "attention" && attention ? `${f.label} ${attention}` : f.label,
|
||||
count: f.value === "attention" && attention ? attention : undefined,
|
||||
}))}
|
||||
/>
|
||||
<FilterSelect
|
||||
@@ -160,13 +160,13 @@ export default function SitesPage() {
|
||||
options={SORTS.filter((x) => x.value !== "name")}
|
||||
/>
|
||||
</div>
|
||||
<Segmented
|
||||
<SegmentedControl
|
||||
label="视图"
|
||||
value={view}
|
||||
onChange={(v) => setView(v as View)}
|
||||
options={[
|
||||
{ value: "cards", label: "卡片", icon: <LayoutGrid className="h-3.5 w-3.5" /> },
|
||||
{ value: "list", label: "列表", icon: <List className="h-3.5 w-3.5" /> },
|
||||
{ value: "cards", label: "卡片", icon: <LayoutGrid /> },
|
||||
{ value: "list", label: "列表", icon: <List /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -224,39 +224,3 @@ export default function SitesPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Segmented({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
options: { value: string; label: string; icon?: React.ReactNode }[];
|
||||
}) {
|
||||
return (
|
||||
<div role="radiogroup" aria-label={label} className="inline-flex gap-0.5 rounded-xl border border-border p-0.5">
|
||||
{options.map((o) => (
|
||||
// biome-ignore lint/a11y/useSemanticElements: 分段按钮
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={value === o.value}
|
||||
onClick={() => onChange(o.value)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1 text-caption transition-colors",
|
||||
value === o.value
|
||||
? "bg-foreground text-background"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{o.icon}
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import { SubscriptionCard, SubscriptionRow } from "~/components/subscriptions/ov
|
||||
import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-form.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { SegmentedControl } from "~/components/ui/segmented.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { legacyRedirectLoader } from "./redirects.ts";
|
||||
|
||||
@@ -74,27 +75,13 @@ function Pills<T extends string>({
|
||||
items: { value: T | ""; label: string; count?: number }[];
|
||||
}) {
|
||||
return (
|
||||
<div role="tablist" aria-label={label} className="flex min-w-0 gap-1 overflow-x-auto scroll-bleed">
|
||||
{items.map((it) => (
|
||||
<button
|
||||
key={it.value || "all"}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={value === it.value}
|
||||
onClick={() => onChange(it.value)}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-3 text-sm font-medium whitespace-nowrap transition-colors duration-micro",
|
||||
value === it.value
|
||||
? "pill-selected text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{it.label}
|
||||
{it.count !== undefined ? (
|
||||
<span className="text-meta text-muted-foreground tabular-nums">{it.count}</span>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
<div className="min-w-0 overflow-x-auto scroll-bleed">
|
||||
<SegmentedControl<T | "all">
|
||||
label={label}
|
||||
value={value || "all"}
|
||||
onChange={(v) => onChange(v === "all" ? "" : v)}
|
||||
options={items.map((it) => ({ value: it.value || "all", label: it.label, count: it.count }))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -207,7 +194,7 @@ export default function SubscriptionsPage() {
|
||||
title="订阅"
|
||||
actions={
|
||||
<>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link to={consolePath("rules")}>
|
||||
<ListChecks />
|
||||
规则
|
||||
@@ -235,46 +222,25 @@ export default function SubscriptionsPage() {
|
||||
<div className="flex items-center gap-1">
|
||||
{calendar ? null : (
|
||||
<Button
|
||||
variant={selecting ? "secondary" : "ghost"}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-pressed={selecting}
|
||||
onClick={() => (selecting ? exitSelect() : setSelecting(true))}
|
||||
>
|
||||
{selecting ? <X /> : null}
|
||||
{selecting ? "退出批量" : "批量"}
|
||||
</Button>
|
||||
)}
|
||||
<fieldset className="m-0 flex rounded-full border border-border p-0.5" aria-label="视图">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="卡片视图"
|
||||
aria-pressed={!calendar && view === "card"}
|
||||
className={cn("rounded-full", !calendar && view === "card" && "bg-foreground/[0.08]")}
|
||||
onClick={() => setView("card")}
|
||||
>
|
||||
<LayoutGrid />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="列表视图"
|
||||
aria-pressed={!calendar && view === "list"}
|
||||
className={cn("rounded-full", !calendar && view === "list" && "bg-foreground/[0.08]")}
|
||||
onClick={() => setView("list")}
|
||||
>
|
||||
<List />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="日历"
|
||||
aria-pressed={calendar}
|
||||
className={cn("rounded-full", calendar && "bg-foreground/[0.08]")}
|
||||
onClick={() => f.set("view", "calendar")}
|
||||
>
|
||||
<CalendarDays />
|
||||
</Button>
|
||||
</fieldset>
|
||||
<SegmentedControl
|
||||
label="视图"
|
||||
value={calendar ? "calendar" : view}
|
||||
onChange={(x) => (x === "calendar" ? f.set("view", "calendar") : setView(x))}
|
||||
options={[
|
||||
{ value: "card", label: "", title: "卡片视图", icon: <LayoutGrid /> },
|
||||
{ value: "list", label: "", title: "列表视图", icon: <List /> },
|
||||
{ value: "calendar", label: "", title: "日历", icon: <CalendarDays /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{calendar ? (
|
||||
@@ -459,7 +425,7 @@ export default function SubscriptionsPage() {
|
||||
) : (
|
||||
renderItems(visible)
|
||||
)}
|
||||
{items.length > 0 || query.hasNextPage ? <Pagination {...paged.pagination} /> : null}
|
||||
{items.length > 0 || query.hasNextPage ? <Pagination {...paged.pagination} hideSingle /> : null}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user