This commit is contained in:
zhx 2026-07-17 20:59:25 +08:00
parent e915a16cb3
commit 31c330e928
3 changed files with 69 additions and 8 deletions

View File

@ -1036,11 +1036,21 @@ function ResourceFormDialog({ disabled, form, loading, mode, onClose, onSubmit,
const handleAnimationSelected = async (file) => {
let detectedMp4Layout = null;
if (isMp4File(file)) {
detectedMp4Layout = await detectMp4AlphaLayout(file);
try {
detectedMp4Layout = await detectMp4AlphaLayout(file);
} catch {
// MP4
detectedMp4Layout = null;
}
}
let detectedProfileCardLayout = null;
if (form.resourceType === "profile_card") {
detectedProfileCardLayout = await detectProfileCardLayout(file);
try {
detectedProfileCardLayout = await detectProfileCardLayout(file);
} catch {
// PAG libpag
detectedProfileCardLayout = null;
}
}
//
return {

View File

@ -19,7 +19,6 @@ import { Mp4Preview } from "@/shared/ui/Mp4Preview.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import styles from "./UploadField.module.css";
const imageAccept = "image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp,.svga,.pag";
const pagWasmURL = "/vendor/libpag.wasm";
let pagModulePromise;
@ -51,12 +50,10 @@ export function UploadField({
const [downloading, setDownloading] = useState(false);
const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
const isImage = kind === "image";
const uploadMode = uploadKind || kind;
const useImageUpload = uploadMode === "image";
const source = localPreview?.url || value;
const assetKind = localPreview?.assetKind || getAssetKind(value, kind);
const displayName = localPreview?.name || getDisplayValue(value);
const inputAccept = accept !== undefined ? accept : useImageUpload && isImage ? imageAccept : undefined;
const inputAccept = accept || undefined;
const canPreviewVideo = Boolean(source && assetKind === "mp4");
const localPreviewURL = localPreview?.url;
const previewMp4Layout = Object.prototype.hasOwnProperty.call(localPreview?.selectionResult || {}, "mp4Layout")
@ -115,7 +112,9 @@ export function UploadField({
return;
}
setLocalPreview((previous) => (previous ? { ...previous, selectionResult } : previous));
const result = useImageUpload ? await uploadImage(file) : await uploadFile(file);
//
// JPEG/PNG/WebP PAG/SVGA/GIF/AVIF
const result = shouldUploadAsImage(file, kind, uploadKind) ? await uploadImage(file) : await uploadFile(file);
if (uploadGeneration !== uploadGenerationRef.current) {
return;
}
@ -550,6 +549,20 @@ function getAssetKind(value, kind, contentType = "") {
return kind === "image" ? "image" : "file";
}
function shouldUploadAsImage(file, kind, uploadKind) {
if (uploadKind !== undefined) {
return uploadKind === "image";
}
if (kind !== "image") {
return false;
}
const extension = getExtension(file?.name);
if (extension) {
return ["jpg", "jpeg", "png", "webp"].includes(extension);
}
return ["image/jpeg", "image/png", "image/webp"].includes(String(file?.type || "").toLowerCase());
}
function isRasterImageExtension(extension) {
return ["avif", "gif", "jpg", "jpeg", "png", "webp"].includes(extension);
}

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { beforeEach, expect, test, vi } from "vitest";
import { downloadResponse } from "@/shared/api/download";
import { uploadFile, uploadImage } from "@/shared/api/upload";
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
@ -14,6 +14,10 @@ vi.mock("@/shared/api/download", () => ({
downloadResponse: vi.fn(async () => undefined),
}));
beforeEach(() => {
vi.clearAllMocks();
});
test("file upload field does not restrict picker type and uses generic upload endpoint", async () => {
const onChange = vi.fn();
const onFileSelected = vi.fn();
@ -38,6 +42,40 @@ test("file upload field does not restrict picker type and uses generic upload en
expect(onChange).toHaveBeenCalledWith("https://media.haiyihy.com/admin/files/effect.bin");
});
test("image-style resource field accepts PAG and routes it through generic upload", async () => {
const onChange = vi.fn();
const { container } = render(
<ToastProvider>
<UploadField kind="image" label="动效素材" value="" onChange={onChange} />
</ToastProvider>,
);
const input = container.querySelector('input[type="file"]');
const file = new File(["pag"], "effect.pag", { type: "application/octet-stream" });
expect(input).not.toHaveAttribute("accept");
fireEvent.change(input, { target: { files: [file] } });
await waitFor(() => expect(uploadFile).toHaveBeenCalledWith(file));
expect(uploadImage).not.toHaveBeenCalled();
expect(onChange).toHaveBeenCalledWith("https://media.haiyihy.com/admin/files/effect.bin");
});
test("standard raster image keeps the strict image upload endpoint", async () => {
const onChange = vi.fn();
const { container } = render(
<ToastProvider>
<UploadField kind="image" label="封面" value="" onChange={onChange} />
</ToastProvider>,
);
const file = new File(["png"], "cover.png", { type: "image/png" });
fireEvent.change(container.querySelector('input[type="file"]'), { target: { files: [file] } });
await waitFor(() => expect(uploadImage).toHaveBeenCalledWith(file));
expect(uploadFile).not.toHaveBeenCalled();
expect(onChange).toHaveBeenCalledWith("https://media.haiyihy.com/admin/images/effect.png");
});
test("mp4 file upload field can open video preview dialog", async () => {
const source = "https://media.haiyihy.com/admin/files/gift.mp4?sign=1";
const onChange = vi.fn();