fix:sample/plate 之前的开发
This commit is contained in:
192
frontend/src/app/(app)/phenotyping/event-image/page.tsx
Normal file
192
frontend/src/app/(app)/phenotyping/event-image/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user