import type { Decision, DecisionOption } from "@umt/contracts"; import { Search } from "lucide-react"; import { useId, useState } from "react"; import { toast } from "sonner"; import { useResolveDecision } from "~/api/decisions.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { FormField } from "~/components/console/form-field.tsx"; import { EntityPicker, type PickedEntity } from "~/components/library/identity-entity-picker.tsx"; import { type AssignedWork, WorkAssignDialog } from "~/components/library/work-assign-dialog.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"; // 事项(decision)的处理按钮:待处理中心取消后(2026-10-06),问题在发生处就地处理(整理记录行内等), // 选项与执行仍走 POST /decisions/{id}/resolve(后端注册的执行钩子:指定作品后重新整理、替换 / 保留两者 / 跳过…)。 /** 处理按钮只用到事项的这几个字段(整理记录里带的是精简版) */ export type DecisionLike = Pick; const str = (v: unknown) => (typeof v === "string" && v.trim() ? v : null); /** 身份类手动指定选项需要的输入(后端在 option.input.requires 标注) */ type Requires = "tmdb" | "mediaEntityId" | null; function requiresOf(option: DecisionOption): Requires { const r = option.input?.requires; return r === "tmdb" || r === "mediaEntityId" ? r : null; } /** 「指定作品」两个选项(本地作品 / TMDB)已并进 AssignWorkButton,其余选项各一个按钮 */ export function plainOptions(d: DecisionLike): DecisionOption[] { return d.options.filter((o) => o.key !== "dismiss" && requiresOf(o) === null); } /** 事项带不带「指定作品」(本地作品或 TMDB) */ export function canAssignWork(d: DecisionLike): boolean { return d.options.some((o) => requiresOf(o) !== null); } export function OptionButton({ d, option, variant = "outline", }: { d: DecisionLike; option: DecisionOption; variant?: "outline" | "default"; }) { const resolve = useResolveDecision(); const uid = useId(); const [note, setNote] = useState(""); const requires = requiresOf(option); const [tmdbType, setTmdbType] = useState<"movie" | "tv">("movie"); const [tmdbId, setTmdbId] = useState(""); const [entity, setEntity] = useState(null); const tmdbOk = /^\d{1,10}$/.test(tmdbId.trim()); const identity = requires === "tmdb" ? tmdbOk ? { tmdb: { type: tmdbType, id: tmdbId.trim() } } : null : requires === "mediaEntityId" ? entity ? { mediaEntityId: entity.id } : null : undefined; return ( {option.label} } title={option.label} description={option.description ? `${option.description}。` : undefined} confirmLabel={option.label} confirmDisabled={identity === null} onConfirm={() => resolve .mutateAsync({ id: d.id, optionKey: option.key, note: note.trim() || undefined, ...(identity ? { identity } : {}), }) .then( () => toast.success(`已处理:${option.label}`), (err) => { toast.error(errorMessage(err, "处理失败")); throw err; }, ) } > {requires === "tmdb" ? (
setTmdbId(e.target.value)} aria-invalid={tmdbId.trim() !== "" && !tmdbOk} />
) : requires === "mediaEntityId" ? ( ) : null}