diff --git a/src/features/resources/pages/ResourceListPage.jsx b/src/features/resources/pages/ResourceListPage.jsx
index 4b1628e..19e7de2 100644
--- a/src/features/resources/pages/ResourceListPage.jsx
+++ b/src/features/resources/pages/ResourceListPage.jsx
@@ -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 {
diff --git a/src/shared/ui/UploadField.jsx b/src/shared/ui/UploadField.jsx
index 1fa8efe..fb2aff0 100644
--- a/src/shared/ui/UploadField.jsx
+++ b/src/shared/ui/UploadField.jsx
@@ -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);
}
diff --git a/src/shared/ui/UploadField.test.jsx b/src/shared/ui/UploadField.test.jsx
index e3f3fc2..ed19855 100644
--- a/src/shared/ui/UploadField.test.jsx
+++ b/src/shared/ui/UploadField.test.jsx
@@ -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(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ 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();