fix(web): visual QA front-end items U4–U6, U13–U16 and copy list
- U4: form save failures toast the first problem and scroll/focus the first invalid field (policy draft, settings cards, resource drawers, subscription form) via lib/form-errors. - U5: FormField keeps 8px between a hint and the next label. - U6: file manager status bar keeps free space left-aligned, clear of the Agent button. - U13: move/copy dialog stops "target is the source folder / inside the source" before requesting a preview (transferTargetProblem + tests). - U14: account popover uses the solid float material. - U15/U16: inbox tile hint shortened; TMDB assign dialog uses the shared Select and labelled fields (type, TMDB ID, note). - Copy: "TMDB 已保存" spacing, API Key block no longer repeats the section title, whole-day intervals read "每 7 天", "最高级别:提示", "延后:已有计划在途", failed records show "未生成目标路径", upstream model column "默认模型". Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2ae780d2a1
commit
082c556a67
18 files changed
+130
-38
No files matched your search
@@ -1088,7 +1088,8 @@ export default function FilesPage() {
|
||||
{totalSize(selected) ? ` · ${formatBytes(totalSize(selected), 1)}` : ""}
|
||||
</span>
|
||||
) : null}
|
||||
{root.usage ? <span className="ml-auto">剩余 {formatBytes(root.usage.freeBytes, 1)}</span> : null}
|
||||
{/* 靠左排:右下角是 Agent 悬浮按钮(QA U6) */}
|
||||
{root.usage ? <span>剩余 {formatBytes(root.usage.freeBytes, 1)}</span> : null}
|
||||
{!root.writable && root.kind === "library" ? <StatusBadge>只读</StatusBadge> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { Decision, DecisionKind, DecisionOption, DecisionStatus } from "@umt/contracts";
|
||||
import { BellOff, Bot, CheckCheck, ChevronRight, Inbox, Radio, RotateCw } from "lucide-react";
|
||||
import { type ReactNode, useMemo, useRef, useState } from "react";
|
||||
import { type ReactNode, useId, useMemo, useRef, useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { decisionKeys, useDecisionSummary, useDecisions, useResolveDecision } from "~/api/decisions.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
|
||||
import { FormField } from "~/components/console/form-field.tsx";
|
||||
import { JsonView } from "~/components/console/json-view.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||
@@ -18,6 +19,7 @@ import { DECISION_KIND, DECISION_STATUS, opts, SEVERITY } from "~/components/lib
|
||||
import { Chip, relTime, Tone } from "~/components/library/parts.tsx";
|
||||
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 { Textarea } from "~/components/ui/textarea.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { actorText } from "~/lib/labels.ts";
|
||||
@@ -111,6 +113,7 @@ function requiresOf(option: DecisionOption): Requires {
|
||||
|
||||
function OptionButton({ d, option }: { d: Decision; option: DecisionOption }) {
|
||||
const resolve = useResolveDecision();
|
||||
const uid = useId();
|
||||
const [note, setNote] = useState("");
|
||||
const requires = requiresOf(option);
|
||||
const [tmdbType, setTmdbType] = useState<"movie" | "tv">("movie");
|
||||
@@ -161,28 +164,36 @@ function OptionButton({ d, option }: { d: Decision; option: DecisionOption }) {
|
||||
}
|
||||
>
|
||||
{requires === "tmdb" ? (
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
aria-label="TMDB 类型"
|
||||
value={tmdbType}
|
||||
onChange={(e) => setTmdbType(e.target.value as "movie" | "tv")}
|
||||
className="h-9 shrink-0 rounded-lg border border-input bg-transparent px-2 text-caption"
|
||||
>
|
||||
<option value="movie">电影</option>
|
||||
<option value="tv">剧集</option>
|
||||
</select>
|
||||
<Input
|
||||
aria-label="TMDB ID"
|
||||
inputMode="numeric"
|
||||
value={tmdbId}
|
||||
onChange={(e) => setTmdbId(e.target.value)}
|
||||
placeholder="TMDB ID,如 872585"
|
||||
/>
|
||||
<div className="grid gap-x-2 sm:grid-cols-[8rem_minmax(0,1fr)]">
|
||||
<FormField label="类型" htmlFor={`${uid}-type`}>
|
||||
<Select value={tmdbType} onValueChange={(v) => setTmdbType(v as "movie" | "tv")}>
|
||||
<SelectTrigger id={`${uid}-type`} className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="movie">电影</SelectItem>
|
||||
<SelectItem value="tv">剧集</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormField>
|
||||
<FormField label="TMDB ID" htmlFor={`${uid}-tmdb`} required hint="纯数字,如 872585">
|
||||
<Input
|
||||
id={`${uid}-tmdb`}
|
||||
inputMode="numeric"
|
||||
value={tmdbId}
|
||||
onChange={(e) => setTmdbId(e.target.value)}
|
||||
aria-invalid={tmdbId.trim() !== "" && !tmdbOk}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
) : requires === "mediaEntityId" ? (
|
||||
<EntityPicker value={entity} onChange={setEntity} />
|
||||
<FormField label="本地作品" required>
|
||||
<EntityPicker value={entity} onChange={setEntity} />
|
||||
</FormField>
|
||||
) : null}
|
||||
<Textarea value={note} onChange={(e) => setNote(e.target.value)} placeholder="备注(可选)" rows={2} />
|
||||
<FormField label="备注" htmlFor={`${uid}-note`} hint="可选,记在处理结果里">
|
||||
<Textarea id={`${uid}-note`} value={note} onChange={(e) => setNote(e.target.value)} rows={2} />
|
||||
</FormField>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
}
|
||||
@@ -358,7 +369,7 @@ export default function InboxPage() {
|
||||
<StatTile
|
||||
label={isOpenView ? "未关闭" : (DECISION_STATUS[status as DecisionStatus]?.label ?? "全部")}
|
||||
value={summary.isPending ? "…" : (summary.data?.total ?? 0)}
|
||||
hint={isOpenView ? "待处理 + 调查中 + 等你决定" : undefined}
|
||||
hint={isOpenView ? "含调查中、等你决定" : undefined}
|
||||
icon={Inbox}
|
||||
active={!kind}
|
||||
onClick={() => set("kind", "")}
|
||||
@@ -368,7 +379,7 @@ export default function InboxPage() {
|
||||
key={k}
|
||||
label={DECISION_KIND[k]}
|
||||
value={v.count}
|
||||
hint={`最高 ${SEVERITY[v.sev].label}`}
|
||||
hint={`最高级别:${SEVERITY[v.sev].label}`}
|
||||
tone={SEVERITY[v.sev].tone}
|
||||
active={kind === k}
|
||||
onClick={() => set("kind", kind === k ? "" : k)}
|
||||
|
||||
@@ -47,13 +47,18 @@ function reasonText(r: { code: string; message: string }) {
|
||||
return r.code && r.code !== r.message && /^[a-z0-9_.-]+$/i.test(r.code) ? `${r.message}(${r.code})` : r.message;
|
||||
}
|
||||
|
||||
function PathBlock({ label, p }: { label: string; p: PublicPathRef }) {
|
||||
/** failed:失败且没有目标路径时直接说"未生成目标路径",不再显示视角与"未知或未授权" */
|
||||
function PathBlock({ label, p, failed = false }: { label: string; p: PublicPathRef; failed?: boolean }) {
|
||||
const noTarget = failed && p.path === null;
|
||||
return (
|
||||
<div className="min-w-0 flex-1 rounded-lg border border-border/70 px-3 py-2">
|
||||
<p className="text-meta text-muted-foreground">
|
||||
{label} · {VIEW_LABEL[p.view]}
|
||||
{label}
|
||||
{noTarget ? "" : ` · ${VIEW_LABEL[p.view]}`}
|
||||
</p>
|
||||
<p className="mt-0.5 font-mono text-caption break-all text-foreground">
|
||||
{p.path ?? (noTarget ? "未生成目标路径" : "未知或未授权")}
|
||||
</p>
|
||||
<p className="mt-0.5 font-mono text-caption break-all text-foreground">{p.path ?? "未知或未授权"}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -98,7 +103,7 @@ function FileRow({ f }: { f: PublicOrganizationFile }) {
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center">
|
||||
<PathBlock label="整理前" p={f.source} />
|
||||
<ArrowRight className="hidden h-4 w-4 shrink-0 text-muted-foreground md:block" />
|
||||
<PathBlock label="整理后" p={f.target} />
|
||||
<PathBlock label="整理后" p={f.target} failed={f.status === "failed"} />
|
||||
</div>
|
||||
{reason ? (
|
||||
<p className={cn("text-caption break-words", f.failure ? "text-destructive" : "text-muted-foreground")}>
|
||||
@@ -196,7 +201,7 @@ export default function OrganizeRecordPage() {
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center">
|
||||
<PathBlock label="整理前" p={r.source} />
|
||||
<ArrowRight className="hidden h-4 w-4 shrink-0 text-muted-foreground md:block" />
|
||||
<PathBlock label="整理后" p={r.target} />
|
||||
<PathBlock label="整理后" p={r.target} failed={r.status === "failed"} />
|
||||
</div>
|
||||
<KvList
|
||||
items={[
|
||||
|
||||
Reference in new issue
Block a user