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"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { FilterBar } from "~/components/console/filter-bar.tsx"; import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx"; import { consolePath } from "~/components/console/nav.ts"; 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 { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; import { EntityPicker, type PickedEntity } from "./identity-entity-picker.tsx"; import { METADATA_SOURCE } from "./labels.ts"; import { Chip, Panel } from "./parts.tsx"; // 别名管理:检索尽量广(原名/译名/简称/站点标题),确认关联仍严格;用户新增的别名 source=user const ALIAS_KIND: Record, string> = { original: "原名", translated: "译名", short: "简称", site_title: "站点标题", romanized: "罗马字", other: "其他", }; const AliasForm = z.object({ alias: z.string().trim().min(1, "填写别名").max(200, "最多 200 字"), kind: AliasKind, language: z .string() .trim() .max(16, "语言代码过长") .regex(/^$|^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/, "如 zh-CN、en"), locked: z.boolean(), }); function AddAlias({ entity }: { entity: PickedEntity }) { const create = useCreateAlias(); const [f, setF] = useState({ alias: "", kind: "short" as z.infer, language: "", locked: true }); const [errors, setErrors] = useState>({}); const submit = async (e: FormEvent) => { e.preventDefault(); const r = AliasForm.safeParse(f); if (!r.success) { setErrors(zodFieldErrors(r.error.issues)); return; } setErrors({}); try { await create.mutateAsync({ mediaEntityId: entity.id, alias: r.data.alias, kind: r.data.kind, language: r.data.language || null, locked: r.data.locked, source: "user", }); toast.success(`已为「${entity.title}」添加别名`); setF((x) => ({ ...x, alias: "" })); } catch (err) { toast.error(errorMessage(err)); } }; return (
setF((x) => ({ ...x, alias: e.target.value }))} placeholder="如:沙丘 2" /> setF((x) => ({ ...x, language: e.target.value }))} placeholder="zh-CN" />
setF((x) => ({ ...x, locked: v }))} />
); } export function AliasesPanel() { const [entity, setEntity] = useState(null); const [q, setQ] = useState(""); const [dq, setDq] = useState(""); useEffect(() => { const t = setTimeout(() => setDq(q.trim()), 250); return () => clearTimeout(t); }, [q]); const list = useAliases({ mediaEntityId: entity?.id, q: dq || undefined }); const del = useDeleteAlias(); const rows = list.data?.pages.flatMap((p) => p.items); const columns: DataColumn[] = [ { key: "alias", header: "别名", cell: (a) => ( {a.locked ? : null} {a.alias} ), }, { key: "kind", header: "类型", cell: (a) => {ALIAS_KIND[a.kind]} }, { key: "lang", header: "语言", hideBelow: "md", cell: (a) => a.language ?? "—" }, { key: "source", header: "来源", hideBelow: "sm", cell: (a) => METADATA_SOURCE[a.source] ?? a.source }, { key: "entity", header: "作品", hideBelow: "md", cell: (a) => ( {a.mediaEntityTitle ?? "查看"} ), }, { key: "act", header: 操作, align: "right", cell: (a) => ( } title={`删除别名「${a.alias}」?`} description="删除后不再用这个别名匹配作品,已确认的外部身份不受影响。" confirmLabel="删除" destructive onConfirm={async () => { try { await del.mutateAsync(a.id); toast.success("别名已删除"); } catch (err) { toast.error(errorMessage(err)); throw err; } }} /> ), }, ]; return (
{entity ? : null}
a.id} loading={list.isPending} error={list.error} onRetry={() => list.refetch()} empty={{ title: entity ? `「${entity.title}」还没有别名` : "没有匹配的别名", description: "别名用于按原始标题、译名、简称匹配作品。", }} hasMore={list.hasNextPage} onLoadMore={() => list.fetchNextPage()} loadingMore={list.isFetchingNextPage} />
); }