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 { 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(path: string, init?: RequestInit): Promise { 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; } 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 { const [programs, germplasm, crossingProjects, plannedCrosses, actualCrosses, observationUnits] = await Promise.all([ loadProgramOptions(), loadGermplasmOptions(), request>("/brapi/v2/crossingprojects?page=0&pageSize=10"), request>("/brapi/v2/plannedcrosses?page=0&pageSize=10"), request>("/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 | null = null; export function invalidateCrossPedigreeCache() { cachedSnapshot = null; inflightSnapshot = null; } export async function loadCrossPedigreeSnapshot(force = false): Promise { 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; }