Files
brapi-java/frontend/src/app/(app)/germplasm/cross-pedigree/crossPedigreeCache.ts
2026-05-28 15:51:39 +08:00

183 lines
5.7 KiB
TypeScript

import type { Cross, CrossParent, CrossingProject, PlannedCross } from "@/lib/api/types.gen";
import {
loadGermplasmOptions,
loadObservationUnitOptions,
loadProgramOptions,
} from "@/services/dropdownCache";
import { getAuthToken } from "@/utils/token";
import {
mapCross,
mapCrossingProject,
mapPlannedCross,
} from "./mappers";
import type {
CrossParentRow,
CrossRecord,
CrossingProjectRecord,
PlannedCrossRecord,
SelectOption,
} from "./types";
interface BrapiListResponse<T> {
result: { data: T[] };
}
export interface CrossPedigreeSnapshot {
programs: SelectOption[];
germplasm: SelectOption[];
observationUnits: SelectOption[];
crossingProjectOptions: SelectOption[];
plannedCrossOptions: SelectOption[];
crossOptions: SelectOption[];
crossingProjects: CrossingProjectRecord[];
plannedCrosses: PlannedCrossRecord[];
actualCrosses: CrossRecord[];
parentRows: CrossParentRow[];
}
const apiBase = () => {
if (typeof window !== "undefined") return "";
return process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000";
};
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const token = getAuthToken();
const response = await fetch(`${apiBase()}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(init?.headers || {}),
},
});
if (!response.ok) {
const detail = await response.text();
throw new Error(detail || `请求失败:${response.status}`);
}
return response.json() as Promise<T>;
}
const flattenCrossParents = (
crossId: string,
crossName: string | null,
planned: boolean,
crossingProjectId: string | null,
crossingProjectName: string | null,
parent: CrossParent | null | undefined,
slot: "parent1" | "parent2",
): CrossParentRow | null => {
if (!parent) return null;
const hasData = parent.parentType || parent.germplasmDbId || parent.observationUnitDbId;
if (!hasData) return null;
return {
id: `${crossId}:${slot}`,
cross_id: crossId,
cross_name: crossName,
planned,
parent_slot: slot,
parent_type: parent.parentType ?? null,
germplasm_id: parent.germplasmDbId ?? null,
germplasm_name: parent.germplasmName ?? null,
observation_unit_id: parent.observationUnitDbId ?? null,
observation_unit_name: parent.observationUnitName ?? null,
crossing_project_id: crossingProjectId,
crossing_project_name: crossingProjectName,
};
};
function buildParentRows(planned: PlannedCrossRecord[], actual: CrossRecord[]): CrossParentRow[] {
const rows: CrossParentRow[] = [];
for (const cross of planned) {
const p1 = flattenCrossParents(
cross.id, cross.name, true, cross.crossing_project_id, cross.crossing_project_name, cross.parent1, "parent1",
);
const p2 = flattenCrossParents(
cross.id, cross.name, true, cross.crossing_project_id, cross.crossing_project_name, cross.parent2, "parent2",
);
if (p1) rows.push(p1);
if (p2) rows.push(p2);
}
for (const cross of actual) {
const p1 = flattenCrossParents(
cross.id, cross.name, false, cross.crossing_project_id, cross.crossing_project_name, cross.parent1, "parent1",
);
const p2 = flattenCrossParents(
cross.id, cross.name, false, cross.crossing_project_id, cross.crossing_project_name, cross.parent2, "parent2",
);
if (p1) rows.push(p1);
if (p2) rows.push(p2);
}
return rows;
}
async function fetchSnapshotFromNetwork(): Promise<CrossPedigreeSnapshot> {
const [programs, germplasm, crossingProjects, plannedCrosses, actualCrosses, observationUnits] = await Promise.all([
loadProgramOptions(),
loadGermplasmOptions(),
request<BrapiListResponse<CrossingProject>>("/brapi/v2/crossingprojects?page=0&pageSize=10"),
request<BrapiListResponse<PlannedCross>>("/brapi/v2/plannedcrosses?page=0&pageSize=10"),
request<BrapiListResponse<Cross>>("/brapi/v2/crosses?page=0&pageSize=10"),
loadObservationUnitOptions().catch(() => [] as SelectOption[]),
]);
const crossingProjectRows = (crossingProjects.result?.data ?? []).map(mapCrossingProject);
const plannedRows = (plannedCrosses.result?.data ?? []).map(mapPlannedCross);
const actualRows = (actualCrosses.result?.data ?? []).map(mapCross);
return {
programs,
germplasm,
observationUnits,
crossingProjectOptions: crossingProjectRows.map((project) => ({
value: project.id,
label: project.name || project.id,
})),
plannedCrossOptions: plannedRows.map((cross) => ({
value: cross.id,
label: cross.name || cross.id,
})),
crossOptions: [...plannedRows, ...actualRows].map((cross) => ({
value: cross.id,
label: `${cross.planned ? "[计划] " : "[实际] "}${cross.name || cross.id}`,
})),
crossingProjects: crossingProjectRows,
plannedCrosses: plannedRows,
actualCrosses: actualRows,
parentRows: buildParentRows(plannedRows, actualRows),
};
}
let cachedSnapshot: CrossPedigreeSnapshot | null = null;
let inflightSnapshot: Promise<CrossPedigreeSnapshot> | null = null;
export function invalidateCrossPedigreeCache() {
cachedSnapshot = null;
inflightSnapshot = null;
}
export async function loadCrossPedigreeSnapshot(force = false): Promise<CrossPedigreeSnapshot> {
if (!force && cachedSnapshot) return cachedSnapshot;
if (!force && inflightSnapshot) return inflightSnapshot;
inflightSnapshot = fetchSnapshotFromNetwork()
.then((snapshot) => {
cachedSnapshot = snapshot;
inflightSnapshot = null;
return snapshot;
})
.catch((error) => {
inflightSnapshot = null;
throw error;
});
return inflightSnapshot;
}
export function getCrossPedigreeSnapshot(): CrossPedigreeSnapshot | null {
return cachedSnapshot;
}