fix:开发完毕
This commit is contained in:
@@ -1,369 +1,7 @@
|
||||
"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";
|
||||
import { PedigreeEdgePanel } from "./PedigreeEdgePanel";
|
||||
|
||||
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/pedigree(parents 字段)
|
||||
</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">
|
||||
parent:当前材料为子代,关联材料为亲本;child:当前材料为亲本,关联材料为子代。
|
||||
</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>
|
||||
);
|
||||
return <PedigreeEdgePanel />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user