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,192 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { CalendarClock, Camera } from "lucide-react";
import { BrapiEntityPage, type BrapiFormField } from "@/components/brapi/BrapiEntityPage";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
createEventRow,
createImageRow,
deleteEventRow,
deleteImageRow,
fetchEventImageOptions,
fetchEventRows,
fetchImageRows,
updateEventRow,
updateImageRow,
} from "./api";
import { ImageFormUpload } from "./components/ImageFormUpload";
import { NONE_SELECT_VALUE, type SelectOption } from "./types";
const eventTypeOptions: SelectOption[] = [
{ value: "observation", label: "observation / 观察测量" },
{ value: "planting", label: "planting / 播种" },
{ value: "fertilizer", label: "fertilizer / 施肥" },
{ value: "irrigation", label: "irrigation / 灌溉" },
{ value: "tillage", label: "tillage / 耕作整地" },
{ value: "chemicals", label: "chemicals / 药剂处理" },
{ value: "weeding", label: "weeding / 除草" },
{ value: "harvest", label: "harvest / 收获" },
{ value: "other", label: "other / 其他" },
];
const mimeTypeOptions: SelectOption[] = [
{ value: "image/jpeg", label: "image/jpeg" },
{ value: "image/png", label: "image/png" },
{ value: "image/gif", label: "image/gif" },
{ value: "image/webp", label: "image/webp" },
{ value: "image/bmp", label: "image/bmp" },
];
const formatDateRange = (start: unknown, end: unknown) => {
const startText = String(start ?? "").trim();
const endText = String(end ?? "").trim();
if (!startText && !endText) return "N/A";
if (startText && endText) return `${startText} ~ ${endText}`;
return startText || endText;
};
const eventTypeLabel = (value: unknown) => {
const text = String(value ?? "").trim();
const match = eventTypeOptions.find((option) => option.value === text);
return match?.label || text || "N/A";
};
export default function EventImagePage() {
const [studyOptions, setStudyOptions] = useState<SelectOption[]>([]);
const [observationUnitOptions, setObservationUnitOptions] = useState<SelectOption[]>([]);
const [observationOptions, setObservationOptions] = useState<SelectOption[]>([]);
const loadOptions = useCallback(async () => {
const options = await fetchEventImageOptions();
setStudyOptions(options.studies);
setObservationUnitOptions(options.observationUnits);
setObservationOptions(options.observations);
}, []);
const loadEvents = useCallback(async () => {
const [, rows] = await Promise.all([loadOptions(), fetchEventRows()]);
return rows as unknown as Record<string, unknown>[];
}, [loadOptions]);
const loadImages = useCallback(async () => {
const [, rows] = await Promise.all([loadOptions(), fetchImageRows()]);
return rows as unknown as Record<string, unknown>[];
}, [loadOptions]);
const eventFields = useMemo<BrapiFormField[]>(() => [
{ key: "id", label: "Event ID", type: "text", required: true, placeholder: "event-001" },
{ key: "event_type", label: "Event Type", type: "select", required: true, options: eventTypeOptions },
{ key: "event_type_db_id", label: "Event Type DbId", type: "text", placeholder: "CO:0000000" },
{
key: "study_db_id",
label: "Study",
type: "select",
options: [{ value: NONE_SELECT_VALUE, label: "No study" }, ...studyOptions],
},
{ key: "start_date", label: "Start Date", type: "date" },
{ key: "end_date", label: "End Date", type: "date" },
{
key: "observation_unit_db_ids",
label: "ObservationUnit IDs",
type: "text",
placeholder: "ou-plot-001, ou-plot-002",
colSpan: 2,
},
{ key: "event_description", label: "Description", type: "textarea", colSpan: 2 },
], [studyOptions]);
const imageFields = useMemo<BrapiFormField[]>(() => [
{ key: "id", label: "Image ID", type: "text", required: true, placeholder: "image-001" },
{ key: "image_name", label: "Image Name", type: "text", required: true, placeholder: "plot-001-canopy" },
{ key: "image_file_name", label: "File Name", type: "text", placeholder: "image-001.jpg" },
{ key: "image_url", label: "Image URL", type: "text", placeholder: "https://example.org/image.jpg", colSpan: 2 },
{ key: "mime_type", label: "MIME Type", type: "select", options: mimeTypeOptions },
{ key: "image_time_stamp", label: "Image Date", type: "date" },
{
key: "study_db_id",
label: "Study",
type: "select",
options: [{ value: NONE_SELECT_VALUE, label: "No study" }, ...studyOptions],
},
{
key: "observation_unit_db_id",
label: "ObservationUnit",
type: "select",
options: [{ value: NONE_SELECT_VALUE, label: "No observation unit" }, ...observationUnitOptions],
},
{
key: "observation_db_ids",
label: "Observation IDs",
type: "text",
placeholder: observationOptions.slice(0, 3).map((option) => option.value).join(", ") || "obs-001, obs-002",
colSpan: 2,
},
{ key: "description", label: "Description", type: "textarea", colSpan: 2 },
], [observationOptions, observationUnitOptions, studyOptions]);
return (
<Tabs defaultValue="events" className="flex min-h-full flex-col gap-4">
<TabsList className="w-full justify-start overflow-x-auto rounded-lg border bg-white p-1 dark:border-slate-800 dark:bg-slate-950 sm:w-fit">
<TabsTrigger value="events" className="gap-2"><CalendarClock className="h-4 w-4" />Events</TabsTrigger>
<TabsTrigger value="images" className="gap-2"><Camera className="h-4 w-4" />Images</TabsTrigger>
</TabsList>
<TabsContent value="events" className="mt-0 min-h-0 flex-1">
<BrapiEntityPage
icon={CalendarClock}
iconBg="bg-gradient-to-br from-fuchsia-500 to-pink-600"
title="Event Management"
description="Manage phenotyping and field operation events through /brapi/v2/events."
addLabel="New Event"
columns={[
{ key: "eventDbId", label: "Event ID" },
{ key: "event_type", label: "Event Type", render: eventTypeLabel },
{ key: "study_db_id", label: "Study" },
{
key: "start_date",
label: "Date Range",
render: (_value, row) => formatDateRange(row.start_date, row.end_date),
},
{ key: "event_description", label: "Description" },
{ key: "observation_unit_db_ids", label: "Observation Units" },
]}
fields={eventFields}
data={[]}
stats={[{ label: "/brapi/v2/events", value: "BrAPI", className: "bg-fuchsia-50 text-fuchsia-700 dark:bg-fuchsia-400/10 dark:text-fuchsia-200" }]}
loadData={loadEvents}
createRecord={(payload) => createEventRow(payload) as unknown as Promise<Record<string, unknown>>}
updateRecord={(id, payload) => updateEventRow(id, payload) as unknown as Promise<Record<string, unknown>>}
deleteRecord={deleteEventRow}
/>
</TabsContent>
<TabsContent value="images" className="mt-0 min-h-0 flex-1">
<BrapiEntityPage
icon={Camera}
iconBg="bg-gradient-to-br from-purple-500 to-indigo-600"
title="Image Management"
description="Manage field image metadata and links through /brapi/v2/images."
addLabel="New Image"
columns={[
{ key: "imageDbId", label: "Image ID" },
{ key: "image_name", label: "Image Name" },
{ key: "study_db_id", label: "Study" },
{ key: "observation_unit_db_id", label: "Observation Unit" },
{ key: "mime_type", label: "MIME Type" },
{ key: "image_time_stamp", label: "Image Date" },
{ key: "image_url", label: "Image URL" },
]}
fields={imageFields}
data={[]}
stats={[{ label: "/brapi/v2/images", value: "BrAPI", className: "bg-purple-50 text-purple-700 dark:bg-purple-400/10 dark:text-purple-200" }]}
loadData={loadImages}
renderFormExtra={(props) => <ImageFormUpload {...props} />}
createRecord={(payload) => createImageRow(payload) as unknown as Promise<Record<string, unknown>>}
updateRecord={(id, payload) => updateImageRow(id, payload) as unknown as Promise<Record<string, unknown>>}
deleteRecord={deleteImageRow}
/>
</TabsContent>
</Tabs>
);
}