feat(web): 记录抽屉与两层导航标准

控制台导航最多两层(侧栏一项 = 一页,页内最多一层页签 / 分段);从列表打开的
记录改成列表页上的右侧抽屉,打开的记录写在 ?open=,刷新、分享、前进后退都能还原,
后退就是关抽屉。标准写在 docs/design/console-ia.md §9,ui-controls.md 引用。

- DetailSheet 加 xl 宽度、标题下操作行;抽屉里再开的 DetailSheet 在同一抽屉里换成子视图(返回 / Esc)
- record-sheet.tsx:useRecordSheet / RecordLink / useOpenRecord / recordPath / taskPath
- 整理记录、订阅、站点(含页签)、下载、日志任务改成抽屉;旧业务任务 / 下载任务 id 在任务抽屉里换算
- 回收站并进文件管理右侧视图(?view=trash);设置左栏只列分区
- 规则组、策略版本、媒体库未识别 / 外部身份、设置实例编辑走 ?open=
- 旧地址 /console/{organize,subscriptions,sites,downloads}/:id、/tasks/:id、/files/trash 跳到列表页抽屉
- 站内链接、铃铛、命令面板、任务时间线改拼新地址;冒烟按 ?open= 检查抽屉

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 14:03:47 +08:00
1 parent dd0f6e2c09
commit 21c55dfb3a
60 files changed
+2768 -2461

No files matched your search

+8 -42
View File
@@ -1,5 +1,5 @@
import { settingsSectionForLegacyTab } from "@umt/contracts";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { Link, redirect, useLocation, useNavigate } from "react-router";
import { PageHeader } from "~/components/console/page-header.tsx";
import { parseSettingsHash, SECTION_GROUPS, settingsHash } from "~/components/settings/sections.tsx";
@@ -45,9 +45,13 @@ export default function SettingsPage() {
// 切分区回到顶部;带节锚点时对齐到该节(有字段时对齐到字段并聚焦)。节内容多是异步加载的(上游列表、能力清单),
// 上方内容变高会把目标挤下去,所以在 2.5 秒内反复校正,直到用户自己滚动为止。
// 打开 / 关闭记录抽屉(?open=,console-ia.md §9)只改 search,不滚动
const lastSearch = useRef(location.search);
// biome-ignore lint/correctness/useExhaustiveDependencies: location.key 让重复点同一锚点也能再跳一次
useEffect(() => {
if (moved) return;
const searchChanged = lastSearch.current !== location.search;
lastSearch.current = location.search;
if (moved || searchChanged) return;
if (!anchor) {
window.scrollTo({ top: 0 });
return;
@@ -91,7 +95,7 @@ export default function SettingsPage() {
<>
<PageHeader title="设置" description="密钥保存后不再显示。" />
<div className="grid gap-6 lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-8">
{/* 窄屏:搜索 + 分区下拉 + 节横排 */}
{/* 窄屏:搜索 + 分区下拉。左栏只有一层(console-ia.md §9):节是页面里的小标题,搜索与 #分区/节 深链仍能定位 */}
<div className="grid gap-3 lg:hidden">
<SettingsSearch />
<Select value={section.id} onValueChange={(id) => navigate({ hash: settingsHash(id) })}>
@@ -113,30 +117,9 @@ export default function SettingsPage() {
</SelectContent>
</Select>
<p className="text-caption text-muted-foreground">{section.description}</p>
{section.anchors.length > 1 ? (
<nav aria-label="本分区目录" className="scroll-bleed -mx-1 overflow-x-auto px-1 pb-1">
<ul className="flex w-max gap-1.5">
{section.anchors.map((a) => (
<li key={a.id}>
<Link
to={{ hash: settingsHash(section.id, a.id) }}
className={cn(
"block rounded-full border px-3 py-1 text-caption whitespace-nowrap transition-colors duration-micro",
anchor === a.id
? "pill-selected text-foreground"
: "border-border/70 text-muted-foreground hover:text-foreground",
)}
>
{a.label}
</Link>
</li>
))}
</ul>
</nav>
) : null}
</div>
{/* 宽屏:搜索 + 左侧分组导航 */}
{/* 宽屏:搜索 + 左侧分组导航(只列分区,不展开节) */}
<div className="hidden lg:block">
<div className="sticky top-20 grid max-h-[calc(100dvh-6rem)] gap-3 overflow-y-auto pb-2">
<SettingsSearch />
@@ -164,23 +147,6 @@ export default function SettingsPage() {
<s.icon className="size-4 shrink-0" aria-hidden />
{s.label}
</Link>
{active && s.anchors.length > 1 ? (
<ul className="mt-0.5 mb-1.5 ml-[1.375rem] grid gap-0.5 border-l border-border/60 pl-3">
{s.anchors.map((a) => (
<li key={a.id}>
<Link
to={{ hash: settingsHash(s.id, a.id) }}
className={cn(
"block rounded-md px-2 py-1 text-caption transition-colors duration-micro hover:text-foreground",
anchor === a.id ? "text-foreground" : "text-muted-foreground",
)}
>
{a.label}
</Link>
</li>
))}
</ul>
) : null}
</li>
);
})}