fix:图片上传成功
This commit is contained in:
@@ -168,9 +168,9 @@ const eventBody = (payload: EventPayload) => ({
|
||||
});
|
||||
|
||||
const imageBody = (payload: ImagePayload) => ({
|
||||
imageURL: requiredText(payload.image_url, "请先上传图片,上传成功后系统会自动填充图片字段"),
|
||||
imageName: requiredText(payload.image_name, "Image name is required"),
|
||||
imageFileName: optionalText(payload.image_file_name),
|
||||
imageURL: optionalText(payload.image_url),
|
||||
mimeType: optionalText(payload.mime_type),
|
||||
description: optionalText(payload.description),
|
||||
studyDbId: optionalText(payload.study_db_id),
|
||||
|
||||
@@ -29,26 +29,17 @@ function formatFileDate(timestamp: number): string {
|
||||
function buildAutoFillPatch(
|
||||
image: UploadedImage,
|
||||
file: File,
|
||||
current: Record<string, unknown>,
|
||||
): Record<string, string> {
|
||||
const baseName = stripExtension(image.filename);
|
||||
const patch: Record<string, string> = {
|
||||
const baseName = stripExtension(image.filename).trim();
|
||||
const imageId = toImageId(baseName);
|
||||
return {
|
||||
id: imageId,
|
||||
image_name: baseName || imageId,
|
||||
image_file_name: image.filename,
|
||||
image_url: image.url,
|
||||
mime_type: image.contentType,
|
||||
image_time_stamp: formatFileDate(file.lastModified),
|
||||
};
|
||||
|
||||
if (!String(current.id ?? "").trim()) {
|
||||
patch.id = toImageId(baseName);
|
||||
}
|
||||
if (!String(current.image_name ?? "").trim()) {
|
||||
patch.image_name = baseName;
|
||||
}
|
||||
if (!String(current.image_time_stamp ?? "").trim()) {
|
||||
patch.image_time_stamp = formatFileDate(file.lastModified);
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
type ImageFormUploadProps = {
|
||||
@@ -57,15 +48,22 @@ type ImageFormUploadProps = {
|
||||
updateFormBatch: (patch: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
export function ImageFormUpload({ formData, updateForm, updateFormBatch }: ImageFormUploadProps) {
|
||||
export function ImageFormUpload({ formData, updateFormBatch }: ImageFormUploadProps) {
|
||||
const imageUrl = String(formData.image_url ?? "").trim();
|
||||
|
||||
const handleUploaded = (image: UploadedImage, file: File) => {
|
||||
updateFormBatch(buildAutoFillPatch(image, file, formData));
|
||||
updateFormBatch(buildAutoFillPatch(image, file));
|
||||
};
|
||||
|
||||
const handleRemoved = () => {
|
||||
updateForm("image_url", "");
|
||||
updateFormBatch({
|
||||
id: "",
|
||||
image_name: "",
|
||||
image_file_name: "",
|
||||
image_url: "",
|
||||
mime_type: "",
|
||||
image_time_stamp: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,6 +32,7 @@ const eventTypeOptions: SelectOption[] = [
|
||||
|
||||
const mimeTypeOptions: SelectOption[] = [
|
||||
{ value: "image/jpeg", label: "image/jpeg" },
|
||||
{ value: "image/jpg", label: "image/jpg" },
|
||||
{ value: "image/png", label: "image/png" },
|
||||
{ value: "image/gif", label: "image/gif" },
|
||||
{ value: "image/webp", label: "image/webp" },
|
||||
@@ -97,12 +98,12 @@ export default function EventImagePage() {
|
||||
], [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: "id", label: "Image ID", type: "text", required: true, readOnly: true, placeholder: "上传图片后自动生成" },
|
||||
{ key: "image_name", label: "Image Name", type: "text", required: true, readOnly: true, placeholder: "上传图片后自动填写" },
|
||||
{ key: "image_file_name", label: "File Name", type: "text", readOnly: true, placeholder: "上传图片后自动填写" },
|
||||
{ key: "image_url", label: "Image URL", type: "text", required: true, readOnly: true, placeholder: "上传图片后自动填写", colSpan: 2 },
|
||||
{ key: "mime_type", label: "MIME Type", type: "select", readOnly: true, options: mimeTypeOptions },
|
||||
{ key: "image_time_stamp", label: "Image Date", type: "date", readOnly: true },
|
||||
{
|
||||
key: "study_db_id",
|
||||
label: "Study",
|
||||
@@ -125,6 +126,14 @@ export default function EventImagePage() {
|
||||
{ key: "description", label: "Description", type: "textarea", colSpan: 2 },
|
||||
], [observationOptions, observationUnitOptions, studyOptions]);
|
||||
|
||||
const validateImageForm = useCallback((payload: Record<string, unknown>) => {
|
||||
const imageUrl = String(payload.image_url ?? "").trim();
|
||||
if (!imageUrl) {
|
||||
return "请先上传图片,上传成功后系统会自动填充带 * 的图片字段。";
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
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">
|
||||
@@ -182,6 +191,7 @@ export default function EventImagePage() {
|
||||
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} />}
|
||||
validateForm={validateImageForm}
|
||||
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}
|
||||
|
||||
@@ -86,12 +86,14 @@ function YearFormField({
|
||||
onFieldChange,
|
||||
placeholder,
|
||||
required,
|
||||
readOnly,
|
||||
}: {
|
||||
fieldKey: string;
|
||||
value: unknown;
|
||||
onFieldChange: (key: string, value: string) => void;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const yearValue = useMemo(() => parseYearValue(value), [value]);
|
||||
const handleChange = useCallback(
|
||||
@@ -108,6 +110,7 @@ function YearFormField({
|
||||
placeholder={placeholder ?? "请选择年份"}
|
||||
clearable={!required}
|
||||
required={required}
|
||||
readOnly={readOnly}
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
@@ -119,12 +122,14 @@ function DateFormField({
|
||||
onFieldChange,
|
||||
placeholder,
|
||||
required,
|
||||
readOnly,
|
||||
}: {
|
||||
fieldKey: string;
|
||||
value: unknown;
|
||||
onFieldChange: (key: string, value: string) => void;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const dateValue = useMemo(() => parseDateValue(value), [value]);
|
||||
const handleChange = useCallback(
|
||||
@@ -141,6 +146,7 @@ function DateFormField({
|
||||
placeholder={placeholder ?? "请选择日期"}
|
||||
clearable={!required}
|
||||
required={required}
|
||||
readOnly={readOnly}
|
||||
showToday
|
||||
className="w-full"
|
||||
/>
|
||||
@@ -186,6 +192,7 @@ interface BrapiEntityPageProps {
|
||||
createRecord?: (payload: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||
updateRecord?: (id: string, payload: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||
deleteRecord?: (id: string) => Promise<void>;
|
||||
validateForm?: (payload: Record<string, unknown>, editingRow: Record<string, unknown> | null) => string | null | undefined;
|
||||
renderQueryForm?: (props: { keyword: string; onKeywordChange: (value: string) => void }) => ReactNode;
|
||||
renderFormExtra?: (props: {
|
||||
formData: Record<string, unknown>;
|
||||
@@ -214,6 +221,7 @@ export function BrapiEntityPage({
|
||||
createRecord,
|
||||
updateRecord,
|
||||
deleteRecord,
|
||||
validateForm,
|
||||
renderQueryForm,
|
||||
renderFormExtra,
|
||||
useEnhancedDialog = false,
|
||||
@@ -225,6 +233,7 @@ export function BrapiEntityPage({
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingRow, setEditingRow] = useState<Record<string, unknown> | null>(null);
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [deletingRow, setDeletingRow] = useState<Record<string, unknown> | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -313,6 +322,7 @@ export function BrapiEntityPage({
|
||||
|
||||
const openAdd = () => {
|
||||
setEditingRow(null);
|
||||
setFormError(null);
|
||||
setFormData({
|
||||
...fields.reduce<Record<string, unknown>>((defaults, field) => {
|
||||
if (field.type === "year") {
|
||||
@@ -334,6 +344,7 @@ export function BrapiEntityPage({
|
||||
const detail = await resolveRecord(row);
|
||||
setEditingRow(detail);
|
||||
setFormData({ ...detail });
|
||||
setFormError(null);
|
||||
setOpen(true);
|
||||
setError(null);
|
||||
} catch (event) {
|
||||
@@ -361,7 +372,15 @@ export function BrapiEntityPage({
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const validationError = validateForm?.(formData, editingRow);
|
||||
if (validationError) {
|
||||
setFormError(validationError);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setFormError(null);
|
||||
try {
|
||||
if (editingRow) {
|
||||
const rowId = resolveRowId(editingRow);
|
||||
@@ -396,7 +415,9 @@ export function BrapiEntityPage({
|
||||
setOpen(false);
|
||||
setError(null);
|
||||
} catch (event) {
|
||||
setError(event instanceof Error ? event.message : "保存失败");
|
||||
const message = event instanceof Error ? event.message : "保存失败";
|
||||
setFormError(message);
|
||||
setError(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -428,10 +449,12 @@ export function BrapiEntityPage({
|
||||
};
|
||||
|
||||
const updateForm = useCallback((key: string, value: string) => {
|
||||
setFormError(null);
|
||||
setFormData((current) => ({ ...current, [key]: value }));
|
||||
}, []);
|
||||
|
||||
const updateFormBatch = useCallback((patch: Record<string, unknown>) => {
|
||||
setFormError(null);
|
||||
setFormData((current) => ({ ...current, ...patch }));
|
||||
}, []);
|
||||
|
||||
@@ -462,6 +485,7 @@ export function BrapiEntityPage({
|
||||
onFieldChange={updateForm}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
readOnly={field.readOnly}
|
||||
/>
|
||||
) : field.type === "date" ? (
|
||||
<DateFormField
|
||||
@@ -470,6 +494,7 @@ export function BrapiEntityPage({
|
||||
onFieldChange={updateForm}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
readOnly={field.readOnly}
|
||||
/>
|
||||
) : field.type === "select" ? (
|
||||
<Select
|
||||
@@ -678,7 +703,10 @@ export function BrapiEntityPage({
|
||||
</div>
|
||||
|
||||
{useEnhancedDialog ? (
|
||||
<EnhancedDialog open={open} onOpenChange={setOpen}>
|
||||
<EnhancedDialog open={open} onOpenChange={(nextOpen) => {
|
||||
setOpen(nextOpen);
|
||||
if (!nextOpen) setFormError(null);
|
||||
}}>
|
||||
<EnhancedDialogContent
|
||||
title={formDialogTitle}
|
||||
defaultWidth={960}
|
||||
@@ -687,6 +715,11 @@ export function BrapiEntityPage({
|
||||
>
|
||||
<DialogBody>
|
||||
<p className="mb-4 text-sm text-muted-foreground">请填写以下字段,带 * 号为必填项。</p>
|
||||
{formError ? (
|
||||
<div className="mb-4 rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive">
|
||||
{formError}
|
||||
</div>
|
||||
) : null}
|
||||
{formFieldsGrid}
|
||||
</DialogBody>
|
||||
<EnhancedDialogFooter className="flex justify-end gap-2">
|
||||
@@ -695,12 +728,20 @@ export function BrapiEntityPage({
|
||||
</EnhancedDialogContent>
|
||||
</EnhancedDialog>
|
||||
) : (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog open={open} onOpenChange={(nextOpen) => {
|
||||
setOpen(nextOpen);
|
||||
if (!nextOpen) setFormError(null);
|
||||
}}>
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formDialogTitle}</DialogTitle>
|
||||
<DialogDescription>请填写以下字段,带 * 号为必填项。</DialogDescription>
|
||||
</DialogHeader>
|
||||
{formError ? (
|
||||
<div className="rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive">
|
||||
{formError}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="py-2">{formFieldsGrid}</div>
|
||||
<DialogFooter className="mt-2">{formDialogFooter}</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user