183 lines
5.7 KiB
TypeScript
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;
|
|
}
|