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) => { const handleAnimationSelected = async (file) => {
let detectedMp4Layout = null; let detectedMp4Layout = null;
if (isMp4File(file)) { if (isMp4File(file)) {
try {
detectedMp4Layout = await detectMp4AlphaLayout(file); detectedMp4Layout = await detectMp4AlphaLayout(file);
} catch {
// MP4
detectedMp4Layout = null;
}
} }
let detectedProfileCardLayout = null; let detectedProfileCardLayout = null;
if (form.resourceType === "profile_card") { if (form.resourceType === "profile_card") {
try {
detectedProfileCardLayout = await detectProfileCardLayout(file); detectedProfileCardLayout = await detectProfileCardLayout(file);
} catch {
// PAG libpag
detectedProfileCardLayout = null;
}
} }
// //
return { return {

View File

@ -19,7 +19,6 @@ import { Mp4Preview } from "@/shared/ui/Mp4Preview.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx";
import styles from "./UploadField.module.css"; 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"; const pagWasmURL = "/vendor/libpag.wasm";
let pagModulePromise; let pagModulePromise;
@ -51,12 +50,10 @@ export function UploadField({
const [downloading, setDownloading] = useState(false); const [downloading, setDownloading] = useState(false);
const [videoPreviewOpen, setVideoPreviewOpen] = useState(false); const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
const isImage = kind === "image"; const isImage = kind === "image";
const uploadMode = uploadKind || kind;
const useImageUpload = uploadMode === "image";
const source = localPreview?.url || value; const source = localPreview?.url || value;
const assetKind = localPreview?.assetKind || getAssetKind(value, kind); const assetKind = localPreview?.assetKind || getAssetKind(value, kind);
const displayName = localPreview?.name || getDisplayValue(value); 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 canPreviewVideo = Boolean(source && assetKind === "mp4");
const localPreviewURL = localPreview?.url; const localPreviewURL = localPreview?.url;
const previewMp4Layout = Object.prototype.hasOwnProperty.call(localPreview?.selectionResult || {}, "mp4Layout") const previewMp4Layout = Object.prototype.hasOwnProperty.call(localPreview?.selectionResult || {}, "mp4Layout")
@ -115,7 +112,9 @@ export function UploadField({
return; return;
} }
setLocalPreview((previous) => (previous ? { ...previous, selectionResult } : previous)); 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) { if (uploadGeneration !== uploadGenerationRef.current) {
return; return;
} }
@ -550,6 +549,20 @@ function getAssetKind(value, kind, contentType = "") {
return kind === "image" ? "image" : "file"; 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) { function isRasterImageExtension(extension) {
return ["avif", "gif", "jpg", "jpeg", "png", "webp"].includes(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 { 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 { downloadResponse } from "@/shared/api/download";
import { uploadFile, uploadImage } from "@/shared/api/upload"; import { uploadFile, uploadImage } from "@/shared/api/upload";
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx"; import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
@ -14,6 +14,10 @@ vi.mock("@/shared/api/download", () => ({
downloadResponse: vi.fn(async () => undefined), downloadResponse: vi.fn(async () => undefined),
})); }));
beforeEach(() => {
vi.clearAllMocks();
});
test("file upload field does not restrict picker type and uses generic upload endpoint", async () => { test("file upload field does not restrict picker type and uses generic upload endpoint", async () => {
const onChange = vi.fn(); const onChange = vi.fn();
const onFileSelected = 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"); 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 () => { test("mp4 file upload field can open video preview dialog", async () => {
const source = "https://media.haiyihy.com/admin/files/gift.mp4?sign=1"; const source = "https://media.haiyihy.com/admin/files/gift.mp4?sign=1";
const onChange = vi.fn(); const onChange = vi.fn();