fix:sample/plate 之前的开发

This commit is contained in:
彭帅
2026-05-28 11:56:17 +08:00
parent fc36bc83e3
commit 8b65de36b8
367 changed files with 57752 additions and 947 deletions

View File

@@ -0,0 +1,369 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { GitBranch, Pencil, Plus, Trash2 } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Dialog,
DialogBody,
DialogContent,
DialogFooter,
} from "@/components/common/shadcn-enhanced";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { EDGE_TYPE_OPTIONS, PARENT_TYPE_OPTIONS, edgeTypeLabel, parentTypeLabel } from "../constants";
import {
buildPedigreeEdgeFormState,
fetchPedigreeEdgeRows,
fetchPedigreeRows,
removePedigreeEdge,
upsertPedigreeEdge,
} from "../api";
import { NONE_SELECT_VALUE, type PedigreeEdgeFormState, type PedigreeEdgeRow, type SelectOption } from "../types";
export function PedigreeEdgeTab() {
const [rows, setRows] = useState<PedigreeEdgeRow[]>([]);
const [nodeOptions, setNodeOptions] = useState<SelectOption[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<PedigreeEdgeFormState | null>(null);
const [editingEdgeId, setEditingEdgeId] = useState<string | null>(null);
const [deletingEdge, setDeletingEdge] = useState<PedigreeEdgeRow | null>(null);
const [deleting, setDeleting] = useState(false);
const loadRows = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [edgeRows, nodes] = await Promise.all([fetchPedigreeEdgeRows(), fetchPedigreeRows()]);
setRows(edgeRows);
setNodeOptions(
nodes
.filter((node) => node.germplasm_id)
.map((node) => ({
value: node.germplasm_id as string,
label: node.germplasm_name || node.germplasm_id || "",
})),
);
} catch (event) {
setError(event instanceof Error ? event.message : "加载失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadRows();
}, [loadRows]);
const filteredRows = useMemo(() => {
const keyword = search.trim().toLowerCase();
if (!keyword) return rows;
return rows.filter((row) =>
[
row.this_node_name,
row.connected_node_name,
row.edge_type,
row.parent_type,
row.this_node_id,
row.connected_node_id,
].some((value) => String(value ?? "").toLowerCase().includes(keyword)),
);
}, [rows, search]);
const openCreate = () => {
if (nodeOptions.length < 2) {
setError("请先在「Pedigree Node 系谱节点」Tab 创建至少两个节点");
return;
}
setEditingEdgeId(null);
setForm(buildPedigreeEdgeFormState());
setDialogOpen(true);
};
const openEdit = (row: PedigreeEdgeRow) => {
if (row.read_only || row.edge_type === "sibling") {
setError("同胞关系为只读展示,请通过 parent 关系维护");
return;
}
setEditingEdgeId(row.id);
setForm(buildPedigreeEdgeFormState(row));
setDialogOpen(true);
};
const handleSave = async () => {
if (!form) return;
setSaving(true);
setError(null);
try {
await upsertPedigreeEdge(form, editingEdgeId ?? undefined);
setDialogOpen(false);
await loadRows();
} catch (event) {
setError(event instanceof Error ? event.message : "保存失败");
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!deletingEdge) return;
setDeleting(true);
setError(null);
try {
await removePedigreeEdge(deletingEdge.id);
setDeletingEdge(null);
await loadRows();
} catch (event) {
setError(event instanceof Error ? event.message : "删除失败");
} finally {
setDeleting(false);
}
};
const showParentType = form?.edge_type === "parent" || form?.edge_type === "child";
return (
<div className="flex min-h-full flex-col gap-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-center gap-3">
<div className={cn("rounded-xl p-2.5", "bg-gradient-to-br from-cyan-500 to-teal-600")}>
<GitBranch className="h-5 w-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-50">Pedigree Edge </h2>
<p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
pedigree_edge parent / child sibling BrAPI
</p>
</div>
</div>
<Button onClick={openCreate} className="shrink-0 gap-1">
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="mb-1">
<span className="rounded-full bg-cyan-50 px-3 py-1 text-xs text-cyan-700 dark:bg-cyan-400/10 dark:text-cyan-200">
BrAPI PUT /brapi/v2/pedigreeparents
</span>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
<div className="mb-4 flex flex-wrap items-center gap-3">
<Input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="搜索当前材料 / 关联材料 / 关系类型"
className="max-w-sm"
/>
</div>
{error ? <p className="mb-3 text-sm text-red-500">{error}</p> : null}
{loading ? (
<Skeleton className="h-40 w-full" />
) : (
<Table>
<TableHeader>
<TableRow className="bg-slate-50 dark:bg-slate-900">
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>parent_type</TableHead>
<TableHead className="w-28 text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRows.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="py-8 text-center text-sm text-slate-400">
parent / child
</TableCell>
</TableRow>
) : (
filteredRows.map((row) => (
<TableRow key={row.id}>
<TableCell>
{edgeTypeLabel(row.edge_type)}
{row.read_only ? (
<span className="ml-2 text-xs text-slate-400"></span>
) : null}
</TableCell>
<TableCell>{row.this_node_name || row.this_node_id}</TableCell>
<TableCell>{row.connected_node_name || row.connected_node_id}</TableCell>
<TableCell>{parentTypeLabel(row.parent_type)}</TableCell>
<TableCell className="text-right">
{!row.read_only ? (
<div className="flex justify-end gap-1">
<Button size="sm" variant="outline" className="gap-1" onClick={() => openEdit(row)}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button
size="sm"
variant="outline"
className="gap-1 text-red-600 hover:text-red-600"
onClick={() => setDeletingEdge(row)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
) : (
<span className="text-xs text-slate-400"></span>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-xl" title={editingEdgeId ? "编辑系谱边" : "新增系谱边"}>
<DialogBody className="space-y-4">
<div>
<Label className="mb-1.5 block text-sm">edge_type </Label>
<Select
value={form?.edge_type || "parent"}
onValueChange={(value) => {
if (!form) return;
setForm({ ...form, edge_type: value });
}}
>
<SelectTrigger><SelectValue placeholder="选择关系类型" /></SelectTrigger>
<SelectContent position="popper" className="z-[110]">
{EDGE_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="mb-1.5 block text-sm"></Label>
<Select
value={form?.this_node_id || NONE_SELECT_VALUE}
onValueChange={(value) => {
if (!form) return;
setForm({ ...form, this_node_id: value });
}}
>
<SelectTrigger><SelectValue placeholder="选择当前材料" /></SelectTrigger>
<SelectContent position="popper" className="z-[110] max-h-60">
<SelectItem value={NONE_SELECT_VALUE}></SelectItem>
{nodeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="mb-1.5 block text-sm"></Label>
<Select
value={form?.connected_node_id || NONE_SELECT_VALUE}
onValueChange={(value) => {
if (!form) return;
setForm({ ...form, connected_node_id: value });
}}
>
<SelectTrigger><SelectValue placeholder="选择关联材料" /></SelectTrigger>
<SelectContent position="popper" className="z-[110] max-h-60">
<SelectItem value={NONE_SELECT_VALUE}></SelectItem>
{nodeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{showParentType ? (
<div>
<Label className="mb-1.5 block text-sm">parent_type </Label>
<Select
value={form?.parent_type || NONE_SELECT_VALUE}
onValueChange={(value) => {
if (!form) return;
setForm({ ...form, parent_type: value });
}}
>
<SelectTrigger><SelectValue placeholder="选择亲本类型" /></SelectTrigger>
<SelectContent position="popper" className="z-[110]">
<SelectItem value={NONE_SELECT_VALUE}> parent_type</SelectItem>
{PARENT_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<p className="text-xs text-slate-500 dark:text-slate-400">
parentchild
</p>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}></Button>
<Button
onClick={handleSave}
disabled={
saving
|| !form
|| form.this_node_id === NONE_SELECT_VALUE
|| form.connected_node_id === NONE_SELECT_VALUE
|| (showParentType && form.parent_type === NONE_SELECT_VALUE)
}
>
{saving ? "保存中..." : "保存"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={Boolean(deletingEdge)} onOpenChange={(open) => !open && setDeletingEdge(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{" "}
{deletingEdge ? edgeTypeLabel(deletingEdge.edge_type) : ""}
{" "}
{deletingEdge?.this_node_name || deletingEdge?.this_node_id}
{" → "}
{deletingEdge?.connected_node_name || deletingEdge?.connected_node_id}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} disabled={deleting}>
{deleting ? "删除中..." : "确认删除"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}