chore(toolchain): upgrade Bun to 1.4.4 and migrate console to TanStack Router

This commit is contained in:
root committed 2026-11-06 00:02:36 +08:00
1 parent b44ebc2ba1
commit 3b9376c75a
134 files changed
+893 -692

No files matched your search

+1 -1
View File
@@ -1 +1 @@
1.4.3
1.4.4
-1
View File
@@ -1,7 +1,6 @@
node_modules/
dist/
build/
.react-router/
coverage/
*.tsbuildinfo
.env
+2 -2
View File
@@ -1,6 +1,6 @@
# 单容器镜像:一个 bun build --compile 出的 umt-server 二进制,同时承担 API(含 web 静态 SPA)与 worker。
# PostgreSQL 不在本镜像内,见 docker-compose.yml。
# 构建:docker build -t uhd-media-tool . 运行时版本锁定 Bun 1.4.3(ADR-007)
# 构建:docker build -t uhd-media-tool . 运行时版本锁定 Bun 1.4.4(ADR-007)
# ffprobe 取自静态构建镜像(单个 static-pie 二进制,约 135 MB;Debian 的 ffmpeg 包要拉 200+ 个依赖、约 450 MB)
ARG FFPROBE_IMAGE=mwader/static-ffmpeg:9.0.2@sha256:7d9bdaaf887f7e6ce6151f67325c344074b5ff1fb75316011c3376503e449a7b
FROM ${FFPROBE_IMAGE} AS ffprobe
@@ -8,7 +8,7 @@ FROM ${FFPROBE_IMAGE} AS ffprobe
# 与 PG 18 服务端同主版本的备份工具,避免 Debian 默认 17 客户端拒绝导出 PG 18。
FROM postgres:18.6-trixie@sha256:5a5a84b19854a9ffaa54082c166ff4ec27473a361e496e5ea167f298f2da9722 AS postgres-tools
FROM oven/bun:1.4.3 AS build
FROM oven/bun:1.4.4 AS build
ARG APT_MIRROR=""
RUN if [ -n "$APT_MIRROR" ]; then sed -i "s|deb.debian.org|$APT_MIRROR|g" /etc/apt/sources.list.d/debian.sources; fi \
&& apt-get update && apt-get install -y --no-install-recommends build-essential libnode-dev && rm -rf /var/lib/apt/lists/*
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Hono } from "hono";
import { serveStatic } from "hono/bun";
/**
* 托管 React Router SPA 构建产物(build/client):存在的文件直接返回,
* 托管 TanStack Router SPA 构建产物(build/client):存在的文件直接返回,
* 其余非 /api 的 GET 回退 index.html 交给前端路由。/api 下的未知路径仍走 JSON 404。
* 注意 hono/deno 的 serveStatic 要求 path 相对 root,写绝对路径会 404。
*/
+6 -6
View File
@@ -31,15 +31,15 @@ export const copyFile: typeof fsp.copyFile = async (a, b, mode) => {
return fsp.copyFile(a, b, mode);
};
export const rm: typeof fsp.rm = async (p, o) => {
export const rm: typeof fsp.rm = (async (p: P, o?: import("node:fs").RmOptions) => {
assertLocalPaths("rm", s(p));
return fsp.rm(p, o);
};
return (fsp.rm as (path: P, options?: import("node:fs").RmOptions) => Promise<void>)(p, o);
}) as typeof fsp.rm;
export const rmdir: typeof fsp.rmdir = async (p, o) => {
export const rmdir: typeof fsp.rmdir = (async (p: P, o?: import("node:fs").RmDirOptions) => {
assertLocalPaths("rmdir", s(p));
return fsp.rmdir(p, o);
};
return (fsp.rmdir as (path: P, options?: import("node:fs").RmDirOptions) => Promise<void>)(p, o);
}) as typeof fsp.rmdir;
export const unlink: typeof fsp.unlink = async (p) => {
assertLocalPaths("unlink", s(p));
+5 -2
View File
@@ -2,11 +2,13 @@ import { defineConfig } from "vitest/config";
// 三组测试,按 groupOrder 依次跑:
// unit —— 纯内存、无 PG,文件并行;
// pg —— 真实 PG、假引擎(每文件独立 schema),文件并行;
// pg —— 真实 PG、假引擎(每文件独立 schema),文件串行;
// serial —— 起真实引擎 / worker / 后台循环 / 子进程的集成与 e2e,文件级串行。并发跑时 worker、轮询、迁移
// 互相抢 CPU 与连接,会把"入库→状态推进"这类时序拉长到超时,串行让每个文件拿到稳定的时序预算。
// 新测试缺省进 pg;起真实 worker / 引擎 / 子进程的加进 SERIAL;确认不碰 PG 的加进 UNIT。
// 并行文件数受开发库连接数约束(max_connections 100,每个 harness 连接池 5–10):MAX_WORKERS 别随手调大。
// OpenWorkflow 0.11 的 CREATE INDEX CONCURRENTLY 使用 1s lock_timeout,即使不同 schema,
// 也会等其它测试的长事务快照;PG 文件串行才能稳定完成迁移。
// unit 文件并行数仍受机器资源约束:MAX_WORKERS 别随手调大。
const UNIT = [
"test/api-registry.test.ts",
"test/organize-in-place-verify.test.ts",
@@ -56,6 +58,7 @@ export default defineConfig({
name: "pg",
include: ["test/**/*.test.ts"],
exclude: [...UNIT, ...SERIAL],
fileParallelism: false,
// 迁移类用例在用例体里建 schema + 迁移,并行时排队等同一把锁,缺省 5s 不够
testTimeout: 60_000,
sequence: { groupOrder: 1 },
+5 -3
View File
@@ -1,20 +1,22 @@
# @umt/web
React Router 8 SPA 控制台(`ssr:false`),沿用 uhd_web 设计系统(token 见 `app/app.css`,规范见 `docs/design/`)。只通过 `/api/v1` 与 server 通信,请求/响应类型一律来自 `@umt/contracts`,不手写重复 DTO。开发端口 5180,`/api` 代理到 http://localhost:4380。
TanStack Router SPA 控制台,沿用 uhd_web 设计系统(token 见 `app/app.css`,规范见 `docs/design/`)。只通过 `/api/v1` 与 server 通信,请求/响应类型一律来自 `@umt/contracts`,不手写重复 DTO。开发端口 5180,`/api` 默认代理到 http://localhost:4390,可用 `VITE_API_TARGET` 覆盖。
## 命令
在仓库根目录执行:
- `bun run dev:web`:开发服务器(5180)。
- `cd apps/web && bun run build`:生产构建到 `build/client`,由 server 的 `WEB_ROOT` 同端口托管。
- `cd apps/web && bun run build`:TanStack Router SPA 生产构建到 `build/client`,由 server 的 `WEB_ROOT` 同端口托管。
- `cd apps/web && bun run test`:单测(`app/**/*.test.ts`)。
- `bun run test:web:smoke`:Playwright 全页面冒烟,前提与环境变量见仓库 README。
- `bun run check:tokens`:设计 token 守卫。
## 目录
- `app/routes/`:页面(`routes.ts` 登记全部路由);`app/routes/console/` 为控制台。
- `app/routes/`:页面(`app/router.tsx` 登记全部路由);`app/routes/console/` 为控制台。
- `index.html` 与 `app/entry.client.tsx`:Vite 浏览器入口;`app/root.tsx` 提供主题、查询缓存与页面标题。
- `app/lib/router-compat.tsx`:现有链接与导航 hook 的兼容入口,查询参数保持字符串和重复值,旧页面的 `clientLoader` 重定向由 `app/router.tsx` 执行。
- `app/api/`:按资源分的 react-query hook,路径与 schema 取 `@umt/contracts` 的 `Api` 注册表。
- `app/components/`:业务组件与 `ui/` 基础组件。
- Agent 页的 `?mock=1` 演示模式只在开发构建或后端 `pi.provider = "fake"` 时生效,生产构建连真实模型时忽略。
@@ -19,7 +19,6 @@ import {
RotateCcw,
} from "lucide-react";
import { type ReactNode, useEffect, useId, useRef, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import {
type AgentRunView,
@@ -47,6 +46,7 @@ import { formatUsd } from "~/components/schedules/labels.ts";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { humanizeReason } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatTokens } from "~/lib/tokens.ts";
import { cn, formatDateTime } from "~/lib/utils.ts";
import { InlineApproval, OptionsCard } from "./agent-interaction.tsx";
@@ -1,6 +1,5 @@
import { Bot, ChevronDown, Maximize2, SquarePen, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Link, useLocation } from "react-router";
import { toast } from "sonner";
import { agentMockEnabled, useAgentRun, useCreateAgentRun } from "~/api/agent.ts";
import { useDownloadTask } from "~/api/downloads.ts";
@@ -11,6 +10,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link, useLocation } from "~/lib/router-compat.tsx";
import { DRAFT_DRAWER_GOAL, writeDraft } from "./agent-nav.ts";
import { AgentRunPanel, GoalComposer } from "./agent-run-panel.tsx";
import { AgentSessionList, drawerListMemory } from "./agent-session-list.tsx";
@@ -2,7 +2,6 @@ import { useQueryClient } from "@tanstack/react-query";
import { type AgentRunStatus, EventType } from "@umt/contracts";
import { Bot, ChevronDown, FlaskConical, ListChecks, RefreshCw, RotateCcw, WifiOff } from "lucide-react";
import { type ReactNode, useId, useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import {
type AgentSessionOptions,
@@ -28,6 +27,7 @@ import { TechInfo } from "~/components/console/tech-info.tsx";
import { formatUsd } from "~/components/schedules/labels.ts";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { useNavigate } from "~/lib/router-compat.tsx";
import { useSse, useSseStatus } from "~/lib/sse.ts";
import { formatTokens } from "~/lib/tokens.ts";
import { cn, formatDateTime } from "~/lib/utils.ts";
@@ -1,6 +1,5 @@
import { Bot, ChevronLeft, ChevronRight, Search } from "lucide-react";
import { useLayoutEffect, useRef, useState } from "react";
import { Link } from "react-router";
import { type AgentRunView, isRunActive, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { useListPagination } from "~/components/console/use-list-pagination.ts";
@@ -9,6 +8,7 @@ import { Input } from "~/components/ui/input.tsx";
import type { PaginationProps } from "~/components/ui/pagination.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import { contextLabel, RunStatusBadge } from "./agent-conversation.tsx";
import { type ListMemory, newListMemory, revealScrollTop } from "./agent-nav.ts";
@@ -2,10 +2,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { AgentRunEvent } from "@umt/contracts";
import { createElement, type ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import type { AgentRunView, GoalReport } from "~/api/agent.ts";
import { qk } from "~/lib/query-keys.ts";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import {
AgentConversation,
claimedCounts,
@@ -1,11 +1,11 @@
import type { AgentRunNode, AgentRunStatus } from "@umt/contracts";
import { ChevronDown, CornerLeftUp, ExternalLink, GitBranch, LoaderCircle } from "lucide-react";
import { createContext, type ReactNode, useContext, useEffect, useId, useState } from "react";
import { Link } from "react-router";
import { agentMockEnabled, isRunActive, useAgentRun } from "~/api/agent.ts";
import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx";
import { LoadingState } from "~/components/console/empty-state.tsx";
import { formatUsd } from "~/components/schedules/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import { AgentConversation, RunStatusBadge } from "./agent-conversation.tsx";
import { shortGoalTitle } from "./agent-store.ts";
@@ -1,10 +1,10 @@
// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/citations.tsx
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
// 改造:去掉 favicon 抓取(控制台链接都是站内),改成可选图标槽;站内链接走 react-router 的 Link(`to`),
// 改造:去掉 favicon 抓取(控制台链接都是站内),改成可选图标槽;站内链接走路由兼容层的 Link(`to`),
// 外链仍新窗口打开;新增 `meta`(右侧状态文字);入场/折叠去 motion,用 Disclosure 与 tw-animate;字号/圆角换 token。
import { BookOpenText, ChevronDown, ExternalLink } from "lucide-react";
import { type ReactNode, useId, useState } from "react";
import { Link } from "react-router";
import { Link } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import { CHEVRON_CLASS, Disclosure } from "./disclosure.tsx";
@@ -1,13 +1,13 @@
import type { BrowserResourceResult, BrowserSessionView } from "@umt/contracts";
import { Download, Settings2 } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useSubmitRelease } from "~/api/releases.ts";
import { ManualDownloadDialog } from "~/components/downloads/manual-download-dialog.tsx";
import { Button } from "~/components/ui/button.tsx";
import { workbenchResource } from "~/components/workbench/download-dialog.tsx";
import { apiData, errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
/** 控件位于 iframe/截图外;直接解析当前详情页并交给共同手动下载入口。 */
export function ResourceDownload({ session }: { session: BrowserSessionView }) {
@@ -1,7 +1,6 @@
import { PasswordChangeRequest, type SessionInfo } from "@umt/contracts";
import { KeyRound, LoaderCircle, LogOut, ShieldCheck, UserRound } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { z } from "zod";
import { useChangePassword, useLogout } from "~/api/auth.ts";
@@ -17,6 +16,7 @@ import {
import { Input } from "~/components/ui/input.tsx";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
import { errorMessage, LOGIN_PATH } from "~/lib/api.ts";
import { useNavigate } from "~/lib/router-compat.tsx";
import { cn, formatDateTime } from "~/lib/utils.ts";
import { FormField, zodFieldErrors } from "./form-field.tsx";
@@ -1,6 +1,5 @@
import { Bot, Keyboard } from "lucide-react";
import { useEffect, useMemo } from "react";
import { useLocation } from "react-router";
import { isRunActive, RUN_STATUS_LABEL, useAgentRuns } from "~/api/agent.ts";
import { AGENT_LAUNCHER_ATTR } from "~/components/agent/agent-drawer.tsx";
import {
@@ -9,6 +8,7 @@ import {
shortGoalTitle,
toggleAgentDrawer,
} from "~/components/agent/agent-store.ts";
import { useLocation } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
// 智能体抽屉的入口只有一个按钮:桌面是侧栏最上方的「智能体」,没有侧栏的窄屏是顶栏胶囊里的智能体按钮
@@ -1,7 +1,6 @@
import type { Decision } from "@umt/contracts";
import { ArrowRight, Bell, CheckCheck } from "lucide-react";
import { useState } from "react";
import { Link, useNavigate } from "react-router";
import { useAttention } from "~/api/attention.ts";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { consolePath } from "~/components/console/nav.ts";
@@ -10,6 +9,7 @@ import { SEVERITY } from "~/components/library/labels.ts";
import { Button } from "~/components/ui/button.tsx";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
import { formatRelative } from "~/lib/labels.ts";
import { Link, useNavigate } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
// 顶栏铃铛(2026-10-06 起只放两类):需要你批准的智能体操作(会话里的审批 / 方案选择、计划审批、原站页面等你接手)
+5 -3
View File
@@ -12,16 +12,18 @@ import {
} from "./nav.ts";
import { recordPath, taskPath } from "./record-sheet.tsx";
/** routes.ts 里登记的控制台路由(相对 /console) */
/** TanStack 路由表里登记的控制台路由(相对 /console) */
const ROUTE_PATTERNS = [
"",
...[...readFileSync(new URL("../../routes.ts", import.meta.url), "utf8").matchAll(/route\("([^"*]+)",/g)].map(
"agent",
"agent/$runId",
...[...readFileSync(new URL("../../router.tsx", import.meta.url), "utf8").matchAll(/page\("([^"*]+)",/g)].map(
(m) => m[1] as string,
),
];
const matchesRoute = (pathname: string) => {
const rest = pathname.replace(/^\/console\/?/, "");
return ROUTE_PATTERNS.some((p) => new RegExp(`^${p.replace(/:[^/]+/g, "[^/]+")}$`).test(rest));
return ROUTE_PATTERNS.some((p) => new RegExp(`^${p.replace(/\$[^/]+/g, "[^/]+")}$`).test(rest));
};
describe("控制台导航", () => {
@@ -1,6 +1,13 @@
import { useCallback, useRef } from "react";
import { Link, type LinkProps, type Location, useLocation, useNavigate, useSearchParams } from "react-router";
import { consolePath } from "~/components/console/nav.ts";
import {
Link,
type LinkProps,
type Location,
useLocation,
useNavigate,
useSearchParams,
} from "~/lib/router-compat.tsx";
// 记录抽屉的地址约定(docs/design/console-ia.md §9):从列表打开的记录 = 列表页上的右侧抽屉,
// 打开的记录写在 ?open=<id>(其余参数不动),刷新、分享、前进后退都能还原;
@@ -1,5 +1,5 @@
import { useCallback } from "react";
import { useSearchParams } from "react-router";
import { useSearchParams } from "~/lib/router-compat.tsx";
/**
* 列表页筛选状态写进 URL query(docs/design/console-ia.md §3.6),刷新和分享链接不丢筛选。
@@ -1,5 +1,5 @@
import { Construction } from "lucide-react";
import { useLocation } from "react-router";
import { useLocation } from "~/lib/router-compat.tsx";
import { EmptyState } from "./empty-state.tsx";
import { findNavItem } from "./nav.ts";
import { PageHeader } from "./page-header.tsx";
@@ -1,8 +1,8 @@
import type { AttentionSummary, Decision } from "@umt/contracts";
import { ChevronRight } from "lucide-react";
import { Link } from "react-router";
import { approvalHref, HEALTH_HREF } from "~/components/console/inbox-bell.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { Link } from "~/lib/router-compat.tsx";
import { ORGANIZE_ATTENTION_PATH } from "~/routes/console/redirects.ts";
// 首页与运行状态共用的「需要关注」汇总计数(2026-10-06 去掉待处理中心后):
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { Dashboard, SystemHealth, SystemResources } from "@umt/contracts";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { qk } from "~/lib/query-keys.ts";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { HomeRuntime } from "./home-runtime.tsx";
// 首页运行状态简版卡:整体健康、CPU、内存、磁盘,以及下载器 / 站点 / 媒体库目录摘要,点进去是运行状态页。
@@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query";
import { type Dashboard, DownloadBoard, type HealthLevel } from "@umt/contracts";
import { ArrowRight, Download, Gauge, type LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { useSitesWithAccounts } from "~/api/dashboard.ts";
import { apiParsed } from "~/api/parse.ts";
import { useSystemHealth, useSystemResources } from "~/api/system.ts";
@@ -11,6 +10,7 @@ import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
import { Sparkline, usageTone } from "~/components/status/resource-section.tsx";
import { formatRelative } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatBytes } from "~/lib/utils.ts";
const LEVEL: Record<HealthLevel, { label: string; tone: StatusTone }> = {
@@ -2,8 +2,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type DiscoverItem, DiscoverItem as DiscoverItemSchema } from "@umt/contracts";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { DiscoverCard } from "./discover-card.tsx";
import {
apiQuery,
@@ -1,9 +1,9 @@
import { ChevronRight } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Link } from "react-router";
import { useDiscoverSection } from "~/api/discover.ts";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { ChipRow } from "./chips.tsx";
import { DiscoverCard, DiscoverCardSkeleton } from "./discover-card.tsx";
import {
@@ -1,7 +1,6 @@
import { type DiscoverItem, type DiscoverMatchCandidate, tmdbImageUrl } from "@umt/contracts";
import { ExternalLink, Search } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Link, useNavigate } from "react-router";
import { toast } from "sonner";
import { useDiscoverMatch } from "~/api/discover.ts";
import { consolePath } from "~/components/console/nav.ts";
@@ -10,6 +9,7 @@ import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-
import { Button } from "~/components/ui/button.tsx";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link, useNavigate } from "~/lib/router-compat.tsx";
import type { CardAction } from "./discover-card.tsx";
import { itemHref } from "./discover-state.ts";
@@ -1,7 +1,6 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { cursorPage, Task, type TaskKind, type TaskStatus } from "@umt/contracts";
import { RotateCcw, Square } from "lucide-react";
import { Link } from "react-router";
import { toast } from "sonner";
import { apiMutate } from "~/api/parse.ts";
import { DetailSection } from "~/components/console/detail-sheet.tsx";
@@ -11,6 +10,7 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { actorText } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatDateTime } from "~/lib/utils.ts";
// 旧业务任务(task 表)的展示零件:引擎只负责推进,task 表是面向用户的投影;取消只停后续步骤,重试只对失败任务。
@@ -1,9 +1,9 @@
import { ExternalLink } from "lucide-react";
import { Link } from "react-router";
import type { TimelineEvent, TimelineStage } from "~/api/tasks.ts";
import { LoadingState } from "~/components/console/empty-state.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Timeline } from "~/components/console/timeline.tsx";
import { Link } from "~/lib/router-compat.tsx";
// 业务时间线(参考 MovieClaw task-activity):把观察 → 计划 → 下载 → 整理 → Agent 串成一个业务过程,
// 阶段计数与首页/侧栏同口径;每条事件都有文字说明,不只靠颜色。
@@ -1,6 +1,5 @@
import type { Dashboard } from "@umt/contracts";
import { Brush, HardDrive, SlidersHorizontal } from "lucide-react";
import { Link, useNavigate } from "react-router";
import { toast } from "sonner";
import { useCacheCleanup, useCacheVolumes } from "~/api/downloads.ts";
import { downloaders } from "~/api/instances.ts";
@@ -12,6 +11,7 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
import { TechInfo } from "~/components/console/tech-info.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link, useNavigate } from "~/lib/router-compat.tsx";
import { cn, formatBytes } from "~/lib/utils.ts";
type Volume = Dashboard["cache"]["volumes"][number];
@@ -7,12 +7,12 @@ import type {
} from "@umt/contracts";
import { ArrowDown, ArrowUp, CheckCircle2, Download } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import type { DataColumn } from "~/components/console/data-table.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { SegmentedControl } from "~/components/ui/segmented.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
import { formatEta, formatSpeed, liveStateLabel, organizeState, peersText, progressOf } from "./board-format.ts";
import { DownloadStatusBadges, PauseResume, ProgressLine } from "./download-detail.tsx";
@@ -1,8 +1,8 @@
import type { PublicOrganizationRecord } from "@umt/contracts";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { expect, it } from "vitest";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { DownloadOrganizeIssue } from "./download-detail.tsx";
const render = (status: PublicOrganizationRecord["status"]) =>
@@ -1,7 +1,6 @@
import type { AssetVersion, DownloadSaveTarget, ManualAcquisitionResult } from "@umt/contracts";
import { ArrowDown, ArrowUp, Bot, ChevronDown, Download, Search, Settings2, X } from "lucide-react";
import { useEffect, useId, useState } from "react";
import { Link, useNavigate } from "react-router";
import { toast } from "sonner";
import { useDownloadSavePaths } from "~/api/downloads.ts";
import { useEntityDetail } from "~/api/library.ts";
@@ -27,6 +26,7 @@ import { Input } from "~/components/ui/input.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
import { absTime, relTime } from "~/components/workbench/release-fields.ts";
import { errorMessage } from "~/lib/api.ts";
import { Link, useNavigate } from "~/lib/router-compat.tsx";
import { cn, formatBytes } from "~/lib/utils.ts";
import {
buildManualOptions,
@@ -1,6 +1,5 @@
import type { DownloadBoardPlanItem } from "@umt/contracts";
import { Check, X } from "lucide-react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useDecideApproval, useIsAdminSession } from "~/api/agent.ts";
import { useCancelPlan, usePlanWhy } from "~/api/downloads.ts";
@@ -14,6 +13,7 @@ import { TechInfo } from "~/components/console/tech-info.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { reasonCode } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { PLAN_WAIT, PURPOSE_LABEL, planWaitHint, whyText } from "./plan-format.ts";
@@ -1,8 +1,8 @@
import type { Decision, EpisodeMappingPreview } from "@umt/contracts";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import {
agentSimulationText,
EpisodeDecisionBody,
@@ -8,7 +8,6 @@ import type {
} from "@umt/contracts";
import { BellOff, Bot, Check, LoaderCircle, PencilLine } from "lucide-react";
import { useId, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useResolveDecision } from "~/api/decisions.ts";
import { useEntityWords, useEpisodeMappingPreview } from "~/api/episode-mapping.ts";
@@ -19,6 +18,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { humanizeReason } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
import { canonicalText, episodeCode, mappingHref, scopeText, sourceText } from "./model.ts";
// 作品页集数映射里的「集数待确认」(episode_mapping 内部事项):来源季集、原因、建议的那一行作品识别词与它的模拟结果;
@@ -1,7 +1,7 @@
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { expect, it, vi } from "vitest";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { EpisodeMappingPanel } from "./mapping-panel.tsx";
vi.mock("~/api/episode-mapping.ts", () => ({
@@ -1,13 +1,13 @@
import type { EpisodeMappingPreview, EpisodeSuggestion } from "@umt/contracts";
import { Wand2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useLocation, useSearchParams } from "react-router";
import { useEntityWords, useEpisodeMappingPreview } from "~/api/episode-mapping.ts";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { WordsEditor } from "~/components/identify-words/words-editor.tsx";
import { Chip, Panel } from "~/components/library/parts.tsx";
import { Button } from "~/components/ui/button.tsx";
import { useLocation, useSearchParams } from "~/lib/router-compat.tsx";
import { EpisodeIssuesList } from "./issues.tsx";
import { groupMethodsText, lineFromSearch, MAPPING_ANCHOR } from "./model.ts";
import { PreviewTable } from "./preview-table.tsx";
@@ -1,10 +1,10 @@
import { ChevronRight, Info } from "lucide-react";
import { Link } from "react-router";
import { useEpisodeMappingPreview } from "~/api/episode-mapping.ts";
import { useEntityDetail } from "~/api/library.ts";
import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Panel } from "~/components/library/parts.tsx";
import { Link } from "~/lib/router-compat.tsx";
import { EpisodeIssueReasons } from "./issues.tsx";
import { absoluteGroups, groupMethodsText, mappingHref } from "./model.ts";
+1 -1
View File
@@ -14,7 +14,6 @@ import {
Wand2,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import {
type FileEntry,
@@ -56,6 +55,7 @@ import {
} from "~/components/ui/dialog.tsx";
import { Input } from "~/components/ui/input.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatBytes } from "~/lib/utils.ts";
import { DirPicker } from "./dir-picker.tsx";
import {
@@ -1,6 +1,5 @@
import { LoaderCircle, ScanSearch } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { type FileEntry, type FileRoot, filesErrorText, useFileStat, useProbe } from "~/api/files.ts";
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, useRecordsForEntity } from "~/api/records.ts";
@@ -13,6 +12,7 @@ import { SLOT_STATUS } from "~/components/library/labels.ts";
import type { WizardEntity } from "~/components/organize-wizard/model.ts";
import { Button } from "~/components/ui/button.tsx";
import { downloadLabel } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { EntryStatus } from "./file-list.tsx";
import { FilePreviewSection } from "./file-preview.tsx";
+1 -1
View File
@@ -19,11 +19,11 @@ import {
Wand2,
} from "lucide-react";
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { Link } from "react-router";
import type { FileEntry } from "~/api/files.ts";
import { RecordLink } from "~/components/console/record-sheet.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatBytes } from "~/lib/utils.ts";
import type { MenuAnchor } from "./context-menu.tsx";
import {
+1 -1
View File
@@ -1,6 +1,5 @@
import { Clock, File as FileIcon, Folder, LoaderCircle, RotateCcw, Trash, Trash2 } from "lucide-react";
import { type ReactNode, useMemo, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useSession } from "~/api/auth.ts";
import {
@@ -33,6 +32,7 @@ import {
import { Input } from "~/components/ui/input.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { actorText } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
const DAY = 86_400_000;
@@ -1,7 +1,6 @@
import type { EntityWordsSummary } from "@umt/contracts";
import { ExternalLink, Plus } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { useEntityWords, useEntityWordsList, useGlobalWords } from "~/api/episode-mapping.ts";
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
import { DetailSheet } from "~/components/console/detail-sheet.tsx";
@@ -22,6 +21,7 @@ import {
DialogTitle,
} from "~/components/ui/dialog.tsx";
import { actorText } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatDateTime } from "~/lib/utils.ts";
import { WordsEditor } from "./words-editor.tsx";
@@ -7,7 +7,6 @@ import {
} from "@umt/contracts";
import { Check, LoaderCircle, Search } from "lucide-react";
import { type FormEvent, useId, useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { z } from "zod";
import { useEntitySearch, useLockField, useMergeEntity, useSetEntityIdentity } from "~/api/library.ts";
@@ -28,6 +27,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~
import { Switch } from "~/components/ui/switch.tsx";
import { Textarea } from "~/components/ui/textarea.tsx";
import { errorMessage } from "~/lib/api.ts";
import { useNavigate } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import { ENTITY_FIELD_LABEL, EVIDENCE_LEVEL, IDENTITY_STATUS, MEDIA_KIND } from "./labels.ts";
import { Tone } from "./parts.tsx";
@@ -1,7 +1,6 @@
import { AliasKind, type TitleAlias } from "@umt/contracts";
import { ExternalLink, Lock, Plus, Trash2 } from "lucide-react";
import { type FormEvent, useEffect, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { z } from "zod";
import { useAliases, useCreateAlias, useDeleteAlias } from "~/api/identity.ts";
@@ -15,6 +14,7 @@ import { Input } from "~/components/ui/input.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { EntityPicker, type PickedEntity } from "./identity-entity-picker.tsx";
import { METADATA_SOURCE } from "./labels.ts";
import { Chip, Panel } from "./parts.tsx";
@@ -1,7 +1,6 @@
import type { EnrichFieldName, IdentityPoolItem, MetadataEnrichOutput } from "@umt/contracts";
import { ExternalLink, LoaderCircle, Sparkles } from "lucide-react";
import { useId, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { enrichPosterUrl, useApplyEnrich, useEnrichPool } from "~/api/identity.ts";
import { DetailSection } from "~/components/console/detail-sheet.tsx";
@@ -10,6 +9,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Label } from "~/components/ui/label.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { ENRICH_FIELD_LABEL, ENRICH_SKIPPED, ENRICH_SOURCE_KIND, EVIDENCE_LEVEL } from "./labels.ts";
import { Chip } from "./parts.tsx";
@@ -1,6 +1,5 @@
import type { ExternalIdentity, IdentityRevision } from "@umt/contracts";
import { ExternalLink, Lock, LockOpen } from "lucide-react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useIdentityRevisions, useUpdateIdentity } from "~/api/identity.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
@@ -11,6 +10,7 @@ import { consolePath } from "~/components/console/nav.ts";
import { Timeline } from "~/components/console/timeline.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatDateTime } from "~/lib/utils.ts";
import { EVIDENCE_LEVEL, IDENTITY_STATUS, VERIFY_STATUS } from "./labels.ts";
import { Tone } from "./parts.tsx";
@@ -1,7 +1,6 @@
import type { IdentityPoolItem, PoolCandidate } from "@umt/contracts";
import { BellOff, ExternalLink, LoaderCircle, RotateCw } from "lucide-react";
import { useId, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useAssignPool, useDismissPool, useRetryPool } from "~/api/identity.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
@@ -14,6 +13,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Input } from "~/components/ui/input.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatDateTime } from "~/lib/utils.ts";
import { EnrichSection } from "./identity-enrich.tsx";
import { EVIDENCE_LEVEL, POOL_REASON, POOL_STATUS } from "./labels.ts";
+1 -1
View File
@@ -1,7 +1,6 @@
import type { LogEntry } from "@umt/contracts";
import { Bot, ExternalLink, History } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { useEngineRun } from "~/api/engine.ts";
import { type AgentContext, openAgentDrawer } from "~/components/agent/agent-store.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
@@ -15,6 +14,7 @@ import { LegacyTaskSheet } from "~/components/tasks/legacy-task.tsx";
import { RunActions, TaskDetailBody, TaskStatusLine } from "~/components/tasks/task-detail.tsx";
import { Button } from "~/components/ui/button.tsx";
import { actorTypeLabel, formatDuration } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatDateTime } from "~/lib/utils.ts";
import {
actorLabel,
+1 -1
View File
@@ -2,10 +2,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LOG_STREAM_EVENT, type LogEntry } from "@umt/contracts";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { LogStreamClient, type LogStreamStatus, upsertEntries } from "~/api/logs.ts";
import { qk } from "~/lib/query-keys.ts";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import LogsPage from "~/routes/console/logs.tsx";
import { agentGoalFor, logBadge, logSubtitle, logsPath, logTitle, objectText, sinceToFrom } from "./labels.ts";
import { agentContextFor } from "./log-detail.tsx";
@@ -1,7 +1,6 @@
import { EventType, type IdentifyCompletedPayload, TmdbMediaDetail } from "@umt/contracts";
import { Eye, FileStack, LoaderCircle, RotateCw, Search } from "lucide-react";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { Link, useNavigate } from "react-router";
import { toast } from "sonner";
import {
type FileRef,
@@ -27,6 +26,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog.tsx";
import { Input } from "~/components/ui/input.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { Link, useNavigate } from "~/lib/router-compat.tsx";
import { useSse } from "~/lib/sse.ts";
import { cn } from "~/lib/utils.ts";
import { candidateEntity, EntityPicker } from "./entity-picker.tsx";
@@ -1,7 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import type { EngineRun } from "@umt/contracts";
import { FileStack, ListOrdered, Loader2, RefreshCw } from "lucide-react";
import { Link } from "react-router";
import { useDownloadTask } from "~/api/downloads.ts";
import { useEngineRun, useEngineRuns, useEngineRunsPage } from "~/api/engine.ts";
import { useScrapeRecords } from "~/api/files.ts";
@@ -16,6 +15,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Pagination } from "~/components/ui/pagination.tsx";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
import { qk } from "~/lib/query-keys.ts";
import { Link } from "~/lib/router-compat.tsx";
import { useSse } from "~/lib/sse.ts";
import { organizeQueueStage, organizeRunInput, scrapeQueueStage } from "./queue-model.ts";
import { queueCountLabel } from "./record-filter-model.ts";
@@ -2,8 +2,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { PublicOrganizationRecord, RecordAttention } from "@umt/contracts";
import { createElement, type ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { expect, it, vi } from "vitest";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { OrganizeRecordSheet } from "./organize-record-sheet.tsx";
const state = vi.hoisted(() => ({ record: null as PublicOrganizationRecord | null }));
@@ -1,7 +1,6 @@
import type { PublicOrganizationFile, PublicPathRef } from "@umt/contracts";
import { ArrowRight, ChevronRight, Trash2, Wand2 } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { ACTION_LABEL, RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecord } from "~/api/records.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
@@ -19,6 +18,7 @@ import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx
import { Button } from "~/components/ui/button.tsx";
import { actorTypeLabel } from "~/lib/labels.ts";
import { reasonCode, reasonLine } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
const FILE_STATUS: Record<PublicOrganizationFile["status"], { label: string; tone: StatusTone }> = {
@@ -1,6 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { FilesScrapeRecordsResult, type ScrapeFileResult } from "@umt/contracts";
import { Link } from "react-router";
import { apiParsed } from "~/api/parse.ts";
import { DetailSection } from "~/components/console/detail-sheet.tsx";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
@@ -9,6 +8,7 @@ import { taskPath } from "~/components/console/record-sheet.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { EXPECTED_SCRAPE_SKIPS, SCRAPE_STATUS, scrapeCounts } from "~/components/library/scrape-panel.tsx";
import { reasonLine } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatDateTime } from "~/lib/utils.ts";
/** 失败的文件,以及缺元数据、认不出集号等不在预期内的跳过 */
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { PublicOrganizationRecord, RecordAttention } from "@umt/contracts";
import { createElement, type ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { recordNeedsRefresh } from "~/api/records.ts";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { attentionLine, primaryAction, RecordAttentionActions } from "./record-attention.tsx";
// 整理记录「需要处理」的行内处理(2026-10-06 去掉待处理中心):原因一句话 + 推荐动作,一键执行。
@@ -1,6 +1,5 @@
import type { PublicOrganizationRecord } from "@umt/contracts";
import { BellOff, Bot, ChevronRight, LoaderCircle, Wand2 } from "lucide-react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useAgentIdentifyRecord, useDismissRecordAttention } from "~/api/records.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
@@ -15,6 +14,7 @@ import { mappingHref } from "~/components/episode-mapping/model.ts";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { humanizeReason, reasonCode } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
import { canReorganize } from "./record-parts.ts";
// 整理记录「需要处理」的行内处理(2026-10-06 去掉待处理中心后,整理侧的问题在这里就地处理):
+1 -1
View File
@@ -1,6 +1,5 @@
import type { RuleGroup } from "@umt/contracts";
import { ListChecks, ShieldOff, Tags, Trash2 } from "lucide-react";
import { useSearchParams } from "react-router";
import { toast } from "sonner";
import { type RuleRefs, useDeleteRuleGroup, useUpdateRuleGroup } from "~/api/rules.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
@@ -13,6 +12,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Pagination } from "~/components/ui/pagination.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { useSearchParams } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import { DEFAULT_CHAIN_LABEL, RULE_KIND, RULE_SOURCE, ruleSummary, SUPPRESS_NOTE, scopeText } from "./labels.ts";
@@ -1,7 +1,6 @@
import { type AgentCheckConfig, CAPABILITY_CATALOG, type KnownCapabilityId, type ScheduledJob } from "@umt/contracts";
import { ChevronRight, History, LoaderCircle, Pencil, Play, RotateCcw, SkipForward, Trash2 } from "lucide-react";
import { type ReactNode, useEffect, useRef, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useDeleteSchedule } from "~/api/schedules.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
@@ -12,6 +11,7 @@ import { Button } from "~/components/ui/button.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { agentRunLabel, formatRelative } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { formatTokens } from "~/lib/tokens.ts";
import { cn } from "~/lib/utils.ts";
import { NextRun, useJobActions } from "./auto-task-list.tsx";
@@ -1,7 +1,6 @@
import type { ScheduledJob, ScheduleGroup, ScheduleSpecInput } from "@umt/contracts";
import { ChevronRight, History, LoaderCircle, Play, RotateCcw, SkipForward } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useRunScheduleNow, useSkipSchedule, useUpdateSchedule } from "~/api/schedules.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
@@ -11,6 +10,7 @@ import { Pagination } from "~/components/ui/pagination.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { formatRelative } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { readCollapseState, writeCollapseState } from "~/lib/ui-prefs.ts";
import { cn } from "~/lib/utils.ts";
import { IntervalSelect } from "./interval-select.tsx";
@@ -1,6 +1,5 @@
import { LoaderCircle, Sparkles } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { useCreateAgentRun } from "~/api/agent.ts";
import { FormField } from "~/components/console/form-field.tsx";
@@ -16,6 +15,7 @@ import {
} from "~/components/ui/dialog.tsx";
import { Textarea } from "~/components/ui/textarea.tsx";
import { errorMessage } from "~/lib/api.ts";
import { useNavigate } from "~/lib/router-compat.tsx";
// 用对话起草巡检:用户一句话描述需求 → 发起智能体对话(起草巡检 Skill)→ 智能体建成未启用的巡检,
// 对话里给「检查并启用」卡片,用户在计划页检查、修改后启用。
@@ -1,6 +1,5 @@
import type { ScheduledJob } from "@umt/contracts";
import { History } from "lucide-react";
import { Link } from "react-router";
import { useScheduleRuns } from "~/api/schedules.ts";
import { DetailSheet } from "~/components/console/detail-sheet.tsx";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
@@ -9,6 +8,7 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
import { useListPagination } from "~/components/console/use-list-pagination.ts";
import { Pagination } from "~/components/ui/pagination.tsx";
import { agentRunLabel } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { describeSchedule, formatClock, formatMs, RUN_STATUS, TRIGGER_LABEL } from "./labels.ts";
/** 历史抽屉:最近 50 次运行(新→旧) */
@@ -2,7 +2,6 @@ import { useQueryClient } from "@tanstack/react-query";
import { SCHEDULE_CHANGED_EVENT, type ScheduledJob } from "@umt/contracts";
import { AlertTriangle, Bot, CalendarClock, Plus, Sparkles } from "lucide-react";
import { type ReactNode, useEffect, useRef, useState } from "react";
import { useLocation } from "react-router";
import { usePiUpstreamModels, usePiUpstreams } from "~/api/pi.ts";
import { useScheduleBoard } from "~/api/schedules.ts";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
@@ -10,6 +9,7 @@ import { useListPagination } from "~/components/console/use-list-pagination.ts";
import { Button } from "~/components/ui/button.tsx";
import { Pagination } from "~/components/ui/pagination.tsx";
import { qk } from "~/lib/query-keys.ts";
import { useLocation } from "~/lib/router-compat.tsx";
import { useSse } from "~/lib/sse.ts";
import { AgentCheckCard, type DefaultUpstream } from "./agent-check-card.tsx";
import { AgentCheckDialog } from "./agent-check-dialog.tsx";
@@ -1,11 +1,11 @@
import { type SearchAction, type SearchWork, tmdbImageUrl } from "@umt/contracts";
import { CornerDownLeft, Film, Globe, LoaderCircle, type LucideIcon, Search } from "lucide-react";
import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react";
import { Link, useNavigate } from "react-router";
import { useSearch } from "~/api/search.ts";
import { settingsPath } from "~/components/console/nav.ts";
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "~/components/ui/dialog.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link, useNavigate } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import { SearchHistory, useSearchHistory } from "./search-history.tsx";
import { actionIcon, mediaKindLabel } from "./search-labels.ts";
@@ -12,7 +12,6 @@ import {
UsersRound,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { type ReleaseSearchFilters, useRefreshReleaseSearch, useReleaseSearch } from "~/api/search.ts";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
@@ -27,6 +26,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~
import { ReleaseDetailSheet } from "~/components/workbench/release-detail-sheet.tsx";
import { absTime, relTime, remainingLabel } from "~/components/workbench/release-fields.ts";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatBytes } from "~/lib/utils.ts";
import { QuickFilterBar } from "./quick-filter-bar.tsx";
import {
@@ -4,8 +4,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { MediaServerInventory, SearchRelease, WorkInventory } from "@umt/contracts";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { parseWorkRef, type ReleaseFilterState, toApiFilters, UnmatchedReleases } from "./release-results.tsx";
import { resolvePick } from "./site-picker-dialog.tsx";
import { inventoryBadge, mediaServerSummary, releaseSearchPath, slotLine, workHref, workRef } from "./work-card.tsx";
@@ -1,7 +1,6 @@
import type { SearchWork } from "@umt/contracts";
import { ArrowRight, Film, PackageSearch } from "lucide-react";
import { useState } from "react";
import { Link, useNavigate } from "react-router";
import { mediaServers } from "~/api/instances.ts";
import { useSearch } from "~/api/search.ts";
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
@@ -9,6 +8,7 @@ import { settingsPath } from "~/components/console/nav.ts";
import { useListPagination } from "~/components/console/use-list-pagination.ts";
import { Button } from "~/components/ui/button.tsx";
import { Pagination } from "~/components/ui/pagination.tsx";
import { Link, useNavigate } from "~/lib/router-compat.tsx";
import { actionIcon } from "./search-labels.ts";
import { SitePickerDialog } from "./site-picker-dialog.tsx";
import { releaseSearchPath, WorkCard, workRef } from "./work-card.tsx";
@@ -1,7 +1,6 @@
import type { SearchSiteMode, Site } from "@umt/contracts";
import { PackageSearch } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router";
import { useSiteOptions } from "~/api/instances.ts";
import { useSettings } from "~/api/settings.ts";
import { consolePath } from "~/components/console/nav.ts";
@@ -15,6 +14,7 @@ import {
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog.tsx";
import { Link } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
// 搜资源前选站点:默认(站点页勾了「默认参与搜索」的)/ 全部启用站点 / 指定站点。
+1 -1
View File
@@ -1,11 +1,11 @@
import type { MediaServerInventory, SearchWork, WorkInventory } from "@umt/contracts";
import { ArrowRight, PackageSearch } from "lucide-react";
import { Link } from "react-router";
import { type MediaServerRef, useMediaServerInventory } from "~/api/search.ts";
import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
import { CoverPlaceholder } from "~/components/library/parts.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Link } from "~/lib/router-compat.tsx";
import { mediaKindLabel } from "./search-labels.ts";
// 作品卡(SEARCH-2 第一阶段):TMDB 海报 / 年份 / 类型 / 简介摘要 + 库内状态;「详情」进作品详情页,「搜索资源」先选站点。
+1 -1
View File
@@ -1,6 +1,5 @@
import { ArrowRight } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { useEnrichStats } from "~/api/identity.ts";
import { useSettings } from "~/api/settings.ts";
import { useSites } from "~/api/sites.ts";
@@ -9,6 +8,7 @@ import { consolePath } from "~/components/console/nav.ts";
import { ENRICH_SOURCE_KIND } from "~/components/library/labels.ts";
import { SearchDefaultDialog } from "~/components/sites/search-default-dialog.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Link } from "~/lib/router-compat.tsx";
import { num } from "./resource-section.tsx";
import { SettingsBlock } from "./settings-block.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type CacheRegionList, DEFAULT_SETTINGS } from "@umt/contracts";
import { createElement } from "react";
import { renderToString } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { qk } from "~/lib/query-keys.ts";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { CacheSection, hitDetailText, hitRateText } from "./cache.tsx";
// 设置 → 缓存:缓存区表显示统一元数据缓存的命中率(最近 7 天、三个进程合计)。
@@ -6,7 +6,6 @@ import {
} from "@umt/contracts";
import { selectAcquisitionPolicy } from "@umt/domain";
import { cloneElement, type ReactElement, useId, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { filesErrorText, useFileRoots } from "~/api/files.ts";
import { downloaders } from "~/api/instances.ts";
@@ -30,6 +29,7 @@ import { Input } from "~/components/ui/input.tsx";
import { Pagination } from "~/components/ui/pagination.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { Textarea } from "~/components/ui/textarea.tsx";
import { Link } from "~/lib/router-compat.tsx";
import { formatDateTime } from "~/lib/utils.ts";
import { SaveBar, SettingsBlock } from "./settings-block.tsx";
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DEFAULT_SETTINGS, DEPRECATED_SETTINGS_FIELDS, type GlobalSettings, SETTINGS_SECTIONS } from "@umt/contracts";
import { createElement } from "react";
import { renderToString } from "react-dom/server";
import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import { qk } from "~/lib/query-keys.ts";
import { MemoryRouter } from "~/lib/router-compat.tsx";
import { cacheRow } from "./cache.tsx";
import { SETTINGS_CARDS, searchSettings } from "./search-index.ts";
import { parseSettingsHash, SECTION_GROUPS, SECTIONS, settingsHash } from "./sections.tsx";
@@ -1,7 +1,7 @@
import { Search, X } from "lucide-react";
import { useId, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router";
import { Input } from "~/components/ui/input.tsx";
import { useNavigate } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import { type SettingsSearchHit, searchSettings } from "./search-index.ts";
import { settingsHash } from "./sections.tsx";
+1 -1
View File
@@ -1,7 +1,7 @@
import { ArrowRight } from "lucide-react";
import { Link } from "react-router";
import { consolePath } from "~/components/console/nav.ts";
import { Button } from "~/components/ui/button.tsx";
import { Link } from "~/lib/router-compat.tsx";
import { BrowserProvidersBlock } from "./browser-providers.tsx";
import { num, str } from "./resource-section.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
+1 -1
View File
@@ -12,7 +12,6 @@ import {
Trash2,
} from "lucide-react";
import { type ReactNode, useState } from "react";
import { useSearchParams } from "react-router";
import { toast } from "sonner";
import { useFeeds } from "~/api/feeds.ts";
import { snapshotStale, useSnapshotSite, useToggleSite, useUpdateSite } from "~/api/sites.ts";
@@ -24,6 +23,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover
import { Switch } from "~/components/ui/switch.tsx";
import { circuitLabel, relTime } from "~/components/workbench/release-fields.ts";
import { errorMessage } from "~/lib/api.ts";
import { useSearchParams } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import type { SiteDialog, useSiteChecks } from "./site-dialogs.tsx";
import {
@@ -1,7 +1,6 @@
import type { SiteAccount, SiteHealthResult, SiteListItem, SiteLoginResult } from "@umt/contracts";
import { CircleCheck, CircleX, KeyRound, LoaderCircle, TriangleAlert } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { downloaders } from "~/api/instances.ts";
import {
@@ -29,6 +28,7 @@ import { Input } from "~/components/ui/input.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import {
CredentialFields,
@@ -1,12 +1,12 @@
import type { AgentSpendBucket, AgentSpendKind } from "@umt/contracts";
import { BellRing, Bot } from "lucide-react";
import { Link } from "react-router";
import { useAgentSpend, useAttention } from "~/api/attention.ts";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { settingsPath } from "~/components/console/nav.ts";
import { AttentionList, attentionRows } from "~/components/dashboard/attention-summary.tsx";
import { formatRelative } from "~/lib/labels.ts";
import { humanizeReason } from "~/lib/reasons.ts";
import { Link } from "~/lib/router-compat.tsx";
// 运行状态页(2026-10-06):「需要关注」汇总(整理需要处理、等你批准、系统健康,系统健康逐条列出、恢复后自动消失)
// 与智能体用量(今天 / 最近 7 天按会话类型;不设上限,只展示)。
@@ -1,7 +1,6 @@
import type { CoverageCell, SubscriptionCoverage } from "@umt/contracts";
import { Clock, Download, ExternalLink, Wand2 } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useFillSlot } from "~/api/subscriptions.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
@@ -9,6 +8,7 @@ import { KvList } from "~/components/console/kv-list.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
import {
CELL_LABEL,
@@ -1,6 +1,5 @@
import { Pause, Pencil, Play, RefreshCw, RotateCcw, Search } from "lucide-react";
import { useMemo, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import {
useRefreshCoverage,
@@ -40,6 +39,7 @@ import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { actorText } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatDateTime } from "~/lib/utils.ts";
// 订阅对比视图(FE-5,docs/design/subscriptions.md §2):季页签 → 集 × 收录版本矩阵;收录版本排序与自动补全;订阅信息。
@@ -1,7 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { AssetOperation, DownloadTask, Task } from "@umt/contracts";
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router";
import { z } from "zod";
import { apiParsed, isMissingEndpoint } from "~/api/parse.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
@@ -11,6 +10,7 @@ import type { StatusTone } from "~/components/console/status-badge.tsx";
import { Timeline } from "~/components/console/timeline.tsx";
import { TASK_KIND_LABEL, TaskActions, TaskStatusLine, TaskSummary } from "~/components/downloads/business-tasks.tsx";
import { TTL_SECOND } from "~/lib/api.ts";
import { useNavigate } from "~/lib/router-compat.tsx";
// 日志页任务抽屉打开的 id 不是引擎运行时(旧链接 /console/tasks/:id):先当旧业务任务查,
// 有引擎运行就换成那条;没有就在抽屉里显示旧任务的概况与文件操作;再查不到当下载任务 id,换到下载面板的抽屉。
@@ -1,7 +1,6 @@
import type { EngineRunDetail, EngineStepAttempt } from "@umt/contracts";
import { Ban, ChevronDown, ExternalLink, Repeat, ScrollText } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { ENGINE_TERMINAL, useCancelEngineRun, useEngineRun, useRetryEngineRun } from "~/api/engine.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
@@ -15,6 +14,7 @@ import { BUSINESS_OBJECT_TYPE, objectLogsPath } from "~/components/logs/labels.t
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { formatDuration, formatRelative, spanSec } from "~/lib/labels.ts";
import { Link } from "~/lib/router-compat.tsx";
import { cn, formatDateTime } from "~/lib/utils.ts";
import {
BUSINESS_KIND_LABEL,
@@ -1,7 +1,6 @@
import type { InventoryEntry, WorkbenchRelease } from "@umt/contracts";
import { ArrowDown, ArrowUp, Bot, Check, RefreshCw } from "lucide-react";
import { useEffect, useRef } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useEntityDetail } from "~/api/inventory.ts";
import { useRefreshReleaseDetail, useReleaseDetail } from "~/api/releases.ts";
@@ -13,6 +12,7 @@ import { KvList } from "~/components/console/kv-list.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link } from "~/lib/router-compat.tsx";
import { useUiPrefs } from "~/lib/ui-prefs.ts";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { DownloadActions } from "./download-dialog.tsx";
+7
View File
@@ -0,0 +1,7 @@
import { RouterProvider } from "@tanstack/react-router";
import { createRoot } from "react-dom/client";
import { initInputModality } from "~/lib/input-modality.ts";
import { router } from "./router.tsx";
initInputModality();
createRoot(document.getElementById("root")!).render(<RouterProvider router={router} />);
+56
View File
@@ -0,0 +1,56 @@
import { createMemoryHistory, createRootRoute, createRoute, createRouter } from "@tanstack/react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import { navigateTo, parseUrlSearch, resolveTo, stringifyUrlSearch } from "./router-compat.tsx";
const current = { pathname: "/console/files", search: "?view=trash", hash: "#old" };
afterEach(() => vi.unstubAllGlobals());
describe("路由目标兼容", () => {
it("对象目标不继承未指定的查询和锚点", () => {
expect(resolveTo({ pathname: "/console/settings", hash: "#account/totp" }, current)).toBe(
"/console/settings#account/totp",
);
expect(resolveTo({ search: "?view=all" }, current)).toBe("/console/files?view=all");
});
it("仅有锚点的字符串目标保留当前查询", () => {
expect(resolveTo("#details", current)).toBe("/console/files?view=trash#details");
});
it("重复定位同一设置字段生成新的导航 key,后退恢复原来的业务 state", async () => {
vi.stubGlobal("window", { origin: "http://localhost" });
vi.stubGlobal("crypto", undefined);
const root = createRootRoute();
const settings = createRoute({ getParentRoute: () => root, path: "/console/settings" });
const history = createMemoryHistory({ initialEntries: ["/console/settings"] });
const router = createRouter({
routeTree: root.addChildren([settings]),
history,
isServer: false,
parseSearch: parseUrlSearch,
stringifySearch: stringifyUrlSearch,
});
const unsubscribe = history.subscribe(() => void router.load());
try {
await router.load();
const target = new URL("/console/settings?tag=one&tag=two#account/totp", window.origin);
const state = { field: "account.username", sheetFrom: "/console/settings" };
await navigateTo(router, target, { state, preventScrollReset: true });
const firstKey = router.state.location.state.__TSR_key;
const firstIndex = history.location.state.__TSR_index;
await navigateTo(router, target, { state, preventScrollReset: true });
expect(router.state.location.state.__TSR_key).not.toBe(firstKey);
expect(history.location.state.__TSR_index).toBe(firstIndex + 1);
expect(router.state.location.state).toMatchObject(state);
expect(router.state.location.search).toEqual({ tag: ["one", "two"] });
expect(router.state.location.hash).toBe("account/totp");
history.back();
await router.load();
expect(router.state.location.state.__TSR_key).toBe(firstKey);
expect(router.state.location.state).toMatchObject(state);
} finally {
unsubscribe();
}
});
});
+185
View File
@@ -0,0 +1,185 @@
import {
type AnyRouter,
Outlet,
useRouter,
useRouterState,
useParams as useTanstackParams,
} from "@tanstack/react-router";
import type { AnchorHTMLAttributes, MouseEvent, ReactNode } from "react";
import { createContext, forwardRef, useCallback, useContext } from "react";
// Keep URL search values as strings while the console's existing links move to TanStack Router.
export function parseUrlSearch(value: string): Record<string, string | string[]> {
const result: Record<string, string | string[]> = Object.create(null);
for (const [key, part] of new URLSearchParams(value)) {
const previous = result[key];
result[key] = previous === undefined ? part : [...(Array.isArray(previous) ? previous : [previous]), part];
}
return result;
}
export function stringifyUrlSearch(search: Record<string, unknown>): string {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(search)) {
if (value == null) continue;
for (const part of Array.isArray(value) ? value : [value]) params.append(key, String(part));
}
const value = params.toString();
return value ? `?${value}` : "";
}
export type Location = {
pathname: string;
search: string;
hash: string;
state: unknown;
key: string;
};
const TestLocation = createContext<Location | null>(null);
let navigationSequence = 0;
declare module "@tanstack/react-router" {
interface HistoryState {
__umtNavigationKey?: string;
}
}
export function useLocation(): Location {
const testLocation = useContext(TestLocation);
// The test provider is fixed for a render tree; production always uses RouterProvider.
if (testLocation) return testLocation;
// biome-ignore lint/correctness/useHookAtTopLevel: provider branch is stable for the component lifetime
const location = useRouterState({ select: (state) => state.location });
return {
pathname: location.pathname,
search: location.searchStr,
hash: location.hash ? (location.hash.startsWith("#") ? location.hash : `#${location.hash}`) : "",
state: location.state,
key: location.state.__TSR_key ?? location.href,
};
}
type To = string | { pathname?: string; search?: string; hash?: string };
type NavigationOptions = { replace?: boolean; state?: unknown; preventScrollReset?: boolean };
export function navigateTo(router: AnyRouter, target: URL, options: NavigationOptions = {}) {
return router.navigate({
to: target.pathname,
search: parseUrlSearch(target.search),
hash: target.hash.slice(1),
replace: options.replace,
resetScroll: !options.preventScrollReset,
// 显式导航即使地址和业务 state 相同,也要更新 key,让设置搜索能再次定位同一字段。
state: {
...((options.state ?? {}) as Record<string, unknown>),
__umtNavigationKey: `${Date.now()}:${++navigationSequence}`,
},
});
}
export function resolveTo(to: To, current: Pick<Location, "pathname" | "search" | "hash">): string {
if (typeof to === "string") {
if (to.startsWith("?") || to.startsWith("#"))
return `${current.pathname}${to.startsWith("#") ? current.search : ""}${to}`;
return to;
}
return `${to.pathname ?? current.pathname}${to.search ?? ""}${to.hash ?? ""}`;
}
export function useNavigate() {
const router = useRouter({ warn: false });
const { pathname, search, hash } = useLocation();
return useCallback(
(to: To | number, options: NavigationOptions = {}) => {
if (typeof to === "number") {
router?.history.go(to);
return;
}
const target = new URL(resolveTo(to, { pathname, search, hash }), window.location.origin);
if (target.origin !== window.location.origin) {
window.location.assign(target.href);
return;
}
if (!router) return;
void navigateTo(router, target, options);
},
[router, pathname, search, hash],
);
}
type SearchInput = URLSearchParams | Record<string, string>;
export function useSearchParams(): [
URLSearchParams,
(next: SearchInput | ((current: URLSearchParams) => SearchInput), options?: NavigationOptions) => void,
] {
const location = useLocation();
const navigate = useNavigate();
const set = useCallback(
(next: SearchInput | ((current: URLSearchParams) => SearchInput), options?: NavigationOptions) => {
const value = typeof next === "function" ? next(new URLSearchParams(location.search)) : next;
const params = value instanceof URLSearchParams ? value : new URLSearchParams(value);
navigate({ search: params.toString() ? `?${params}` : "" }, options);
},
[location.search, navigate],
);
return [new URLSearchParams(location.search), set];
}
export function useParams<T extends Record<string, string | undefined> = Record<string, string | undefined>>(): T {
const testLocation = useContext(TestLocation);
if (testLocation) return {} as T;
// biome-ignore lint/correctness/useHookAtTopLevel: provider branch is stable for the component lifetime
return useTanstackParams({ strict: false } as never) as T;
}
export type LinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
to: To;
replace?: boolean;
state?: unknown;
preventScrollReset?: boolean;
};
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
{ to, replace, state, preventScrollReset, onClick, target, children, ...props },
ref,
) {
const location = useLocation();
const navigate = useNavigate();
const href = resolveTo(to, location);
const click = (event: MouseEvent<HTMLAnchorElement>) => {
onClick?.(event);
if (
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
target === "_blank" ||
props.download
)
return;
const url = new URL(href, window.location.origin);
if (url.origin !== window.location.origin) return;
event.preventDefault();
navigate(href, { replace, state, preventScrollReset });
};
return (
<a {...props} ref={ref} href={href} target={target} onClick={click}>
{children}
</a>
);
});
export function redirect(to: string): Response {
return new Response(null, { status: 302, headers: { Location: to } });
}
export { Outlet };
// Existing component tests mount only a memory navigation context.
export function MemoryRouter({ children, initialEntries }: { children?: ReactNode; initialEntries?: string[] }) {
const url = new URL(initialEntries?.[0] ?? "/", "http://localhost");
const location: Location = { pathname: url.pathname, search: url.search, hash: url.hash, state: null, key: "test" };
return <TestLocation value={location}>{children}</TestLocation>;
}
+24 -75
View File
@@ -1,92 +1,41 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Outlet, useRouterState } from "@tanstack/react-router";
import { ThemeProvider } from "next-themes";
import { useState } from "react";
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
import { useEffect, useState } from "react";
import { pageTitle } from "~/components/console/nav.ts";
import { GlassTooltip } from "~/components/glass-tooltip.tsx";
import { Toaster } from "~/components/ui/sonner.tsx";
import { initInputModality } from "~/lib/input-modality.ts";
import type { Route } from "./+types/root";
import "@fontsource-variable/inter-tight";
import "@fontsource-variable/space-grotesk";
import "./app.css";
// 触屏 / 鼠标判断要在首屏渲染前定下来(写到 <html data-input>),见 input-modality.ts
if (typeof window !== "undefined") initInputModality();
export const meta: Route.MetaFunction = () => [{ title: "UHD 媒体自动化控制台" }];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<Meta />
<Links />
</head>
<body suppressHydrationWarning>
<ThemeProvider
attribute="class"
defaultTheme="dark"
themes={["light", "dark"]}
enableSystem={false}
storageKey="umt-theme"
>
{children}
<ScrollRestoration />
<Scripts />
{/* 玻璃提示条不叠放(叠起来后面的字会透出来):展开排列、间隔 8px;状态色只在图标上,不开 richColors */}
<Toaster position="top-center" duration={1500} expand gap={8} />
<GlassTooltip />
</ThemeProvider>
</body>
</html>
);
}
export default function App() {
const pathname = useRouterState({ select: (state) => state.location.pathname });
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000, refetchOnWindowFocus: false },
},
defaultOptions: { queries: { staleTime: 30_000, refetchOnWindowFocus: false } },
}),
);
useEffect(() => {
const title = pathname === "/login" ? "登录" : pageTitle(pathname);
document.title = title ? `${title} · UHD 媒体自动化控制台` : "UHD 媒体自动化控制台";
}, [pathname]);
// SPA 首屏主题由 index.html 初始化;next-themes 的服务端引导脚本在客户端保持惰性。
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
);
}
export function HydrateFallback() {
return null;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "出错了";
let details = "发生了未预期的错误。";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "错误";
details = error.status === 404 ? "页面不存在。" : error.statusText || details;
} else if (import.meta.env.DEV && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="container mx-auto p-4 pt-16">
<h1 className="text-title font-semibold">{message}</h1>
<p className="mt-2 text-muted-foreground">{details}</p>
{stack && (
<pre className="mt-4 w-full overflow-x-auto p-4 text-label">
<code>{stack}</code>
</pre>
)}
</main>
<ThemeProvider
attribute="class"
defaultTheme="dark"
themes={["light", "dark"]}
enableSystem={false}
storageKey="umt-theme"
scriptProps={{ type: "text/plain" }}
>
<QueryClientProvider client={queryClient}>
<Outlet />
<Toaster position="top-center" duration={1500} expand gap={8} />
<GlassTooltip />
</QueryClientProvider>
</ThemeProvider>
);
}
+134
View File
@@ -0,0 +1,134 @@
import { createRootRoute, createRoute, createRouter, lazyRouteComponent, redirect } from "@tanstack/react-router";
import type { ComponentType } from "react";
import { EmptyState } from "./components/console/empty-state.tsx";
import { Button } from "./components/ui/button.tsx";
import { parseUrlSearch, stringifyUrlSearch } from "./lib/router-compat.tsx";
import App from "./root.tsx";
type PageModule = { default: ComponentType; clientLoader?: (args: { request: Request }) => Promise<unknown> | unknown };
const modules = import.meta.glob<PageModule>("./routes/**/*.tsx");
async function loadPage(modulePath: string, href: string) {
const module = await modules[`./routes/${modulePath}.tsx`]?.();
if (!module) throw new Error(`路由模块不存在: ${modulePath}`);
const result = await module.clientLoader?.({ request: new Request(new URL(href, window.location.origin)) });
if (result instanceof Response && result.status >= 300 && result.status < 400) {
const target = new URL(result.headers.get("Location") ?? "/console", window.location.origin);
throw redirect({
to: target.pathname,
search: parseUrlSearch(target.search),
hash: target.hash.slice(1),
replace: true,
} as never);
}
}
const rootRoute = createRootRoute({
component: App,
notFoundComponent: lazyRouteComponent(() => modules["./routes/not-found.tsx"]!()),
errorComponent: () => (
<main className="grid min-h-screen place-items-center bg-background px-4">
<EmptyState
title="页面加载失败"
description="刷新页面后重试。"
action={<Button onClick={() => window.location.reload()}>刷新页面</Button>}
/>
</main>
),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
beforeLoad: () => {
throw redirect({ to: "/console" } as never);
},
});
const loginRoute = createRoute({
getParentRoute: () => rootRoute,
path: "login",
component: lazyRouteComponent(() => modules["./routes/login.tsx"]!()),
});
const consoleRoute = createRoute({
getParentRoute: () => rootRoute,
path: "console",
component: lazyRouteComponent(() => modules["./routes/console/layout.tsx"]!()),
});
function page(path: string, modulePath: string) {
return createRoute({
getParentRoute: () => consoleRoute,
path,
component: lazyRouteComponent(() => modules[`./routes/console/${modulePath}.tsx`]!()),
beforeLoad: ({ location }) => loadPage(`console/${modulePath}`, location.href),
});
}
const agentRoute = createRoute({
getParentRoute: () => consoleRoute,
path: "agent",
component: lazyRouteComponent(() => modules["./routes/console/agent.tsx"]!()),
});
const agentIndexRoute = createRoute({
getParentRoute: () => agentRoute,
path: "/",
component: lazyRouteComponent(() => modules["./routes/console/agent._index.tsx"]!()),
});
const agentRunRoute = createRoute({
getParentRoute: () => agentRoute,
path: "$runId",
component: lazyRouteComponent(() => modules["./routes/console/agent.$runId.tsx"]!()),
});
const routeTree = rootRoute.addChildren([
indexRoute,
loginRoute,
consoleRoute.addChildren([
page("/", "home"),
page("inbox", "inbox"),
page("library", "library"),
page("library/$id", "library.$id"),
page("media/tmdb/$type/$id", "media.tmdb.$type.$id"),
page("identity", "identity"),
page("organize", "organize"),
page("organize/$id", "organize.$id"),
page("files", "files"),
page("files/trash", "files.trash"),
page("discover", "discover"),
page("subscriptions", "subscriptions"),
page("subscriptions/$id", "subscriptions.$id"),
page("preferences", "preferences"),
page("sites", "sites"),
page("sites/$siteId", "sites.$siteId"),
page("sources", "sources"),
page("rules", "rules"),
page("downloads", "downloads"),
page("downloads/plans", "downloads.plans"),
page("downloads/plans/$id", "downloads.plans"),
page("downloads/$id", "downloads.$id"),
page("tasks", "tasks"),
page("tasks/$id", "tasks.$id"),
agentRoute.addChildren([agentIndexRoute, agentRunRoute]),
page("engine", "engine"),
page("engine/$runId", "engine.$runId"),
page("audit", "audit"),
page("logs", "logs"),
page("settings", "settings"),
page("schedules", "schedules"),
page("search", "search"),
page("status", "status"),
]),
]);
export const router = createRouter({
routeTree,
parseSearch: parseUrlSearch,
stringifySearch: stringifyUrlSearch,
scrollRestoration: true,
defaultPreload: false,
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
-51
View File
@@ -1,51 +0,0 @@
import { index, layout, prefix, type RouteConfig, route } from "@react-router/dev/routes";
// 显式路由表(W3-1 维护,其他工作区不改这里)。
// 控制台全部挂在 /console 下;/ 重定向到 /console;/login 为登录页(不经过控制台外壳)。
// 新增工作区:在这里加一行 + components/console/nav.ts 加导航项。
export default [
index("routes/index-redirect.tsx"),
route("login", "routes/login.tsx"),
...prefix("console", [
layout("routes/console/layout.tsx", [
index("routes/console/home.tsx"),
route("inbox", "routes/console/inbox.tsx"),
route("library", "routes/console/library.tsx"),
route("library/:id", "routes/console/library.$id.tsx"),
route("media/tmdb/:type/:id", "routes/console/media.tmdb.$type.$id.tsx"),
route("identity", "routes/console/identity.tsx"),
route("organize", "routes/console/organize.tsx"),
route("organize/:id", "routes/console/organize.$id.tsx"),
route("files", "routes/console/files.tsx"),
route("files/trash", "routes/console/files.trash.tsx"),
route("discover", "routes/console/discover.tsx"),
route("subscriptions", "routes/console/subscriptions.tsx"),
route("subscriptions/:id", "routes/console/subscriptions.$id.tsx"),
route("preferences", "routes/console/preferences.tsx"),
route("sites", "routes/console/sites.tsx"),
route("sites/:siteId", "routes/console/sites.$siteId.tsx"),
route("sources", "routes/console/sources.tsx"),
route("rules", "routes/console/rules.tsx"),
route("downloads", "routes/console/downloads.tsx"),
route("downloads/plans", "routes/console/downloads.plans.tsx"),
route("downloads/plans/:id", "routes/console/downloads.plans.tsx", { id: "downloads-plan-legacy" }),
route("downloads/:id", "routes/console/downloads.$id.tsx"),
route("tasks", "routes/console/tasks.tsx"),
route("tasks/:id", "routes/console/tasks.$id.tsx"),
// 智能体:列表页与会话页共用外壳(标题 + 左栏会话列表),切换时只换右栏
route("agent", "routes/console/agent.tsx", [
index("routes/console/agent._index.tsx"),
route(":runId", "routes/console/agent.$runId.tsx"),
]),
route("engine", "routes/console/engine.tsx"),
route("engine/:runId", "routes/console/engine.$runId.tsx"),
route("audit", "routes/console/audit.tsx"),
route("logs", "routes/console/logs.tsx"),
route("settings", "routes/console/settings.tsx"),
route("schedules", "routes/console/schedules.tsx"),
route("search", "routes/console/search.tsx"),
route("status", "routes/console/status.tsx"),
]),
]),
route("*", "routes/not-found.tsx"),
] satisfies RouteConfig;
+1 -1
View File
@@ -1,10 +1,10 @@
import { ArrowLeft, PanelRightOpen } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router";
import { agentMockEnabled } from "~/api/agent.ts";
import { agentListHref } from "~/components/agent/agent-nav.ts";
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
import { agentReturnPath, showAgentRunInDrawer } from "~/components/agent/agent-store.ts";
import { Button } from "~/components/ui/button.tsx";
import { Link, useNavigate, useParams } from "~/lib/router-compat.tsx";
// 会话页的右栏:和抽屉里同一个会话组件(顶部状态与统计、会话流、底部输入区);标题与左栏会话列表在外壳 agent.tsx,
// 切换会话不重挂载。子会话在会话流里派出它的位置。
+1 -1
View File
@@ -1,10 +1,10 @@
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";
import { useNavigate } from "~/lib/router-compat.tsx";
// 智能体会话页的右栏:新对话(和抽屉里同一个组件;标题与左栏会话列表在外壳 agent.tsx)
export default function AgentIndexPage() {
+1 -1
View File
@@ -1,10 +1,10 @@
import { FlaskConical } from "lucide-react";
import { Outlet, useParams } from "react-router";
import { agentMockEnabled } from "~/api/agent.ts";
import { agentRunHref, pageListMemory } from "~/components/agent/agent-nav.ts";
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
import { PageHeader } from "~/components/console/page-header.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Outlet, useParams } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
/**
+1 -1
View File
@@ -1,4 +1,3 @@
import { useSearchParams } from "react-router";
import { PageHeader } from "~/components/console/page-header.tsx";
import {
type ExploreState,
@@ -10,6 +9,7 @@ import {
import { ExploreView } from "~/components/discover/explore-view.tsx";
import { RecommendView } from "~/components/discover/recommend-view.tsx";
import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx";
import { useSearchParams } from "~/lib/router-compat.tsx";
// 发现(对照 MP 推荐 + 探索,docs/porting/moviepilot-discover.md):
// 「推荐」按分区横排 TMDB / 豆瓣 / Bangumi 的热门与榜单,每区「查看全部」带参数切到「探索」;
+1 -1
View File
@@ -3,7 +3,6 @@ import type { DownloadBoardItem, DownloadBoardRow, DownloadBoardState, DownloadT
import { EventType } from "@umt/contracts";
import { RefreshCw } from "lucide-react";
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "react-router";
import { useBoardPlanRow, useDownloadBoard, useDownloadTask, usePlan } from "~/api/downloads.ts";
import { isMissingEndpoint } from "~/api/parse.ts";
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
@@ -25,6 +24,7 @@ import { DownloadDetail } from "~/components/downloads/download-detail.tsx";
import { DROPPED_STATUS, droppedReason } from "~/components/downloads/plan-format.ts";
import { PlanDetail, WhySection } from "~/components/downloads/plan-row.tsx";
import { Button } from "~/components/ui/button.tsx";
import { useSearchParams } from "~/lib/router-compat.tsx";
import { useSse } from "~/lib/sse.ts";
export const meta = () => [{ title: "下载面板 · UHD 媒体自动化控制台" }];
+1 -1
View File
@@ -35,7 +35,6 @@ import {
X,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { toast } from "sonner";
import { useSession } from "~/api/auth.ts";
import {
@@ -102,6 +101,7 @@ 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 { useNavigate, useSearchParams } from "~/lib/router-compat.tsx";
import { useSse } from "~/lib/sse.ts";
import { cn, formatBytes } from "~/lib/utils.ts";
+1 -1
View File
@@ -14,7 +14,6 @@ import {
} from "lucide-react";
import type { ReactNode } from "react";
import { useRef } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useAttention } from "~/api/attention.ts";
import { useDashboard, useSitesWithAccounts } from "~/api/dashboard.ts";
@@ -34,6 +33,7 @@ import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { formatRelative } from "~/lib/labels.ts";
import { qk } from "~/lib/query-keys.ts";
import { Link } from "~/lib/router-compat.tsx";
import { useSse } from "~/lib/sse.ts";
import { cn } from "~/lib/utils.ts";
+2 -2
View File
@@ -1,7 +1,6 @@
import type { SessionInfo } from "@umt/contracts";
import { Clapperboard, LoaderCircle, Menu, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { Link, Outlet, useLocation, useMatches } from "react-router";
import { useSession } from "~/api/auth.ts";
import { AgentDrawer, AgentDrawerTrigger } from "~/components/agent/agent-drawer.tsx";
import { outletKey } from "~/components/agent/agent-store.ts";
@@ -18,6 +17,7 @@ import { ThemeToggle } from "~/components/theme-toggle.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "~/components/ui/sheet.tsx";
import { redirectToLogin } from "~/lib/api.ts";
import { Link, Outlet, useLocation } from "~/lib/router-compat.tsx";
import { cn } from "~/lib/utils.ts";
const PORTAL_NAME = "媒体自动化";
@@ -173,7 +173,7 @@ function ConsoleShell({ session }: { session: SessionInfo }) {
const [sheetOpen, setSheetOpen] = useState(false);
const activeLabel = pageTitle(location.pathname) ?? PORTAL_NAME;
// 路由 handle.layout === "wide"(文件管理):内容区不限最大宽度、不留外边距,页面自己排版
const wide = useMatches().some((m) => (m.handle as { layout?: string } | undefined)?.layout === "wide");
const wide = location.pathname === "/console/files";
return (
<div className="app-scrollbars relative isolate flex min-h-screen overflow-x-clip bg-background">
+1 -1
View File
@@ -27,7 +27,6 @@ import {
Tv,
} from "lucide-react";
import { useMemo, useState } from "react";
import { Link, useParams } from "react-router";
import { toast } from "sonner";
import { mediaServers } from "~/api/instances.ts";
import {
@@ -94,6 +93,7 @@ import { CELL_LABEL, type CellKind, cellKind } from "~/components/subscriptions/
import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-form.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { Link, useParams } from "~/lib/router-compat.tsx";
import { useUiPrefs } from "~/lib/ui-prefs.ts";
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more