修复详情替换产品图上传预览

This commit is contained in:
Your Name 2026-02-05 19:36:25 +08:00
parent 35a1b75df9
commit 049f01059f

View File

@ -92,6 +92,7 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
const [productName, setProductName] = useState<string>(initialProductName || ""); // 产品名称(如"沙发")
const [scenes, setScenes] = useState<SceneItem[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [isProductUploading, setIsProductUploading] = useState(false);
const [aspectRatio, setAspectRatio] = useState<string>("auto"); // 图片比例
const [imageSize, setImageSize] = useState<string>("2K"); // 图片分辨率
@ -99,6 +100,7 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
const productInputRef = useRef<HTMLInputElement>(null);
const sceneInputRef = useRef<HTMLInputElement>(null);
const productPreviewObjectUrlRef = useRef<string | null>(null);
const productUploadIdRef = useRef(0);
// 如果外部传入了初始产品图,则在首次挂载或传入值变化时自动带入
const updateProductPreview = (previewUrl: string | null, isObjectUrl = false) => {
@ -115,6 +117,14 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
const clearProductImage = () => {
setProductImage(null);
updateProductPreview(null);
setIsProductUploading(false);
productUploadIdRef.current += 1;
};
const handleProductPreviewError = () => {
if (productImage && productImagePreview !== productImage) {
updateProductPreview(productImage);
}
};
useEffect(() => {
@ -134,8 +144,36 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
}, []);
// 处理产品图上传
const handleProductUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
const uploadProductImageFile = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/files/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
let errorMessage = "上传失败";
try {
const errorData = await response.json();
errorMessage = errorData?.error || errorData?.message || errorMessage;
} catch {
// ignore
}
throw new Error(errorMessage);
}
const result = await response.json();
if (!result?.url) {
throw new Error("上传失败");
}
return result.url as string;
};
const handleProductUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.target;
const file = input.files?.[0];
if (!file) return;
if (file.size > 10 * 1024 * 1024) {
@ -146,9 +184,12 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
setProductImage(null);
const previewUrl = URL.createObjectURL(file);
updateProductPreview(previewUrl, true);
setIsProductUploading(true);
const uploadId = ++productUploadIdRef.current;
const reader = new FileReader();
reader.onload = (e) => {
if (uploadId !== productUploadIdRef.current) return;
const result = e.target?.result;
if (typeof result === "string") {
setProductImage(result);
@ -158,12 +199,29 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
}
};
reader.onerror = () => {
if (uploadId !== productUploadIdRef.current) return;
toast.error("图片读取失败,请重试");
clearProductImage();
};
reader.readAsDataURL(file);
try {
toast.info("正在上传产品图...");
const uploadedUrl = await uploadProductImageFile(file);
if (uploadId === productUploadIdRef.current) {
setProductImage(uploadedUrl);
updateProductPreview(uploadedUrl);
toast.success("产品图上传成功");
}
} catch (error) {
toast.error(error instanceof Error ? error.message : "产品图上传失败");
} finally {
if (uploadId === productUploadIdRef.current) {
setIsProductUploading(false);
}
}
// 重置 input,允许重复选择同一文件
e.target.value = "";
input.value = "";
};
// 处理场景图上传
@ -632,11 +690,19 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-base font-medium">产品图 A (必须)</Label>
{(productImagePreview || productImage) && (
<Button variant="ghost" size="sm" onClick={clearProductImage} className="h-6 text-xs text-muted-foreground hover:text-destructive">
清除
</Button>
)}
<div className="flex items-center gap-2">
{isProductUploading && (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
上传中...
</span>
)}
{(productImagePreview || productImage) && (
<Button variant="ghost" size="sm" onClick={clearProductImage} className="h-6 text-xs text-muted-foreground hover:text-destructive">
清除
</Button>
)}
</div>
</div>
<div
@ -656,7 +722,12 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
{(productImagePreview || productImage) ? (
<div className="relative aspect-square w-full max-w-[200px] mx-auto">
<img src={productImagePreview || productImage || ""} alt="产品图" className="w-full h-full object-contain rounded-md" />
<img
src={productImagePreview || productImage || ""}
alt="产品图"
className="w-full h-full object-contain rounded-md"
onError={handleProductPreviewError}
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center rounded-md">
<Button variant="secondary" size="sm" onClick={(e) => { e.stopPropagation(); productInputRef.current?.click(); }}>
更换图片
@ -823,7 +894,7 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
className="w-full"
size="lg"
onClick={startFusion}
disabled={isProcessing || !productImage || scenes.length === 0}
disabled={isProcessing || isProductUploading || !productImage || scenes.length === 0}
>
{isProcessing ? (
<>