fix: adapt frontend image paths for backend port 8002 and use blob download

Backend port changed from 8000 to 8002 due to port conflict (frpc on Windows).
- Add resolveImageUrl() to convert relative image URLs to full backend URLs
- Fix all spatial page image src and download handlers to use backend address
- Fix chat message image rendering to use correct port
- Switch download to blob-based approach to support cross-origin file saving

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 20:52:48 +08:00
parent a6987ff996
commit bbf6b921af
10 changed files with 171 additions and 127 deletions
@@ -12,7 +12,7 @@ import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Slider } from "@/components/ui/slider";
import { Loader2, Download, Copy, Upload, Edit3, Wand2, Expand, Palette, Image as ImageIcon, X } from "lucide-react";
import { imageAPI } from "@/lib/api";
import { imageAPI, resolveImageUrl } from "@/lib/api";
import { toast } from "sonner";
interface EditResult {
@@ -312,14 +312,22 @@ export default function ImageToImagePage() {
}
};
const handleDownload = (imageUrl: string, imageId: string) => {
const link = document.createElement("a");
link.href = imageUrl;
link.download = `edited-image-${imageId}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success("图像下载成功");
const handleDownload = async (imageUrl: string, imageId: string) => {
try {
const res = await fetch(imageUrl);
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `edited-image-${imageId}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success("图像下载成功");
} catch {
toast.error("图像下载失败");
}
};
return (
@@ -595,7 +603,7 @@ export default function ImageToImagePage() {
<div className="space-y-4">
<div className="relative group">
<img
src={editResult.url}
src={resolveImageUrl(editResult.url)}
alt="Edited image"
className="w-full h-64 object-cover rounded-lg"
/>
@@ -604,7 +612,7 @@ export default function ImageToImagePage() {
<Button
size="sm"
variant="secondary"
onClick={() => handleDownload(editResult.url, editResult.id)}
onClick={() => handleDownload(resolveImageUrl(editResult.url), editResult.id)}
>
<Download className="h-3 w-3" />
</Button>
@@ -648,7 +656,7 @@ export default function ImageToImagePage() {
>
<div className="relative">
<img
src={variation.url}
src={resolveImageUrl(variation.url)}
alt={`Variation ${index + 1}`}
className="w-full h-48 object-cover rounded-lg"
/>
@@ -657,7 +665,7 @@ export default function ImageToImagePage() {
<Button
size="sm"
variant="secondary"
onClick={() => handleDownload(variation.url, variation.id)}
onClick={() => handleDownload(resolveImageUrl(variation.url), variation.id)}
>
<Download className="h-3 w-3" />
</Button>
+23 -15
View File
@@ -32,7 +32,7 @@ import {
X
} from "lucide-react";
import LoadingSpinner from "@/components/ui/loading-spinner";
import { imageAPI } from "@/lib/api";
import { imageAPI, resolveImageUrl } from "@/lib/api";
import { toast } from "sonner";
import { motion } from "framer-motion";
@@ -350,14 +350,22 @@ export default function SpatialPage() {
}
};
const handleDownload = (imageUrl: string, imageId: string) => {
const link = document.createElement("a");
link.href = imageUrl;
link.download = `image-${imageId}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success("图像下载成功");
const handleDownload = async (imageUrl: string, imageId: string) => {
try {
const res = await fetch(imageUrl);
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `image-${imageId}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success("图像下载成功");
} catch {
toast.error("图像下载失败");
}
};
if (authLoading) {
@@ -580,7 +588,7 @@ export default function SpatialPage() {
<Card className="overflow-hidden">
<div className="aspect-square relative bg-muted">
<img
src={image.url}
src={resolveImageUrl(image.url)}
alt={`Generated image ${index + 1}`}
className="w-full h-full object-cover"
loading="lazy"
@@ -599,7 +607,7 @@ export default function SpatialPage() {
variant="outline"
size="sm"
className="w-full"
onClick={() => handleDownload(image.url, image.id)}
onClick={() => handleDownload(resolveImageUrl(image.url), image.id)}
>
<Download className="h-4 w-4 mr-2" />
@@ -852,7 +860,7 @@ export default function SpatialPage() {
<div className="space-y-4">
<div className="relative group bg-muted rounded-lg overflow-hidden">
<img
src={editResult.url}
src={resolveImageUrl(editResult.url)}
alt="Edited image"
className="w-full h-64 object-contain"
loading="lazy"
@@ -871,7 +879,7 @@ export default function SpatialPage() {
variant="outline"
size="sm"
className="w-full"
onClick={() => handleDownload(editResult.url, editResult.id)}
onClick={() => handleDownload(resolveImageUrl(editResult.url), editResult.id)}
>
<Download className="h-4 w-4 mr-2" />
@@ -907,7 +915,7 @@ export default function SpatialPage() {
<Card className="overflow-hidden">
<div className="relative bg-muted">
<img
src={variation.url}
src={resolveImageUrl(variation.url)}
alt={`Variation ${index + 1}`}
className="w-full h-48 object-cover"
loading="lazy"
@@ -922,7 +930,7 @@ export default function SpatialPage() {
variant="outline"
size="sm"
className="w-full"
onClick={() => handleDownload(variation.url, variation.id)}
onClick={() => handleDownload(resolveImageUrl(variation.url), variation.id)}
>
<Download className="h-4 w-4 mr-2" />
下载图片
@@ -11,7 +11,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Loader2, Download, Copy, RefreshCw, Sparkles, Image as ImageIcon } from "lucide-react";
import { imageAPI } from "@/lib/api";
import { imageAPI, resolveImageUrl } from "@/lib/api";
import { toast } from "sonner";
interface GeneratedImage {
@@ -95,14 +95,22 @@ export default function TextToImagePage() {
}
};
const handleDownload = (imageUrl: string, imageId: string) => {
const link = document.createElement("a");
link.href = imageUrl;
link.download = `generated-image-${imageId}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success("图像下载成功");
const handleDownload = async (imageUrl: string, imageId: string) => {
try {
const res = await fetch(imageUrl);
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `generated-image-${imageId}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success("图像下载成功");
} catch {
toast.error("图像下载失败");
}
};
const handleCopyPrompt = (prompt: string) => {
@@ -316,7 +324,7 @@ export default function TextToImagePage() {
<Card className="overflow-hidden">
<div className="aspect-square relative">
<img
src={image.url}
src={resolveImageUrl(image.url)}
alt={`Generated image ${index + 1}`}
className="w-full h-full object-cover"
/>
@@ -326,7 +334,7 @@ export default function TextToImagePage() {
<Button
size="sm"
variant="secondary"
onClick={() => handleDownload(image.url, image.id)}
onClick={() => handleDownload(resolveImageUrl(image.url), image.id)}
>
<Download className="h-3 w-3" />
</Button>
+1 -1
View File
@@ -254,7 +254,7 @@ export default function MessageItem({ message, selectedModel }: MessageItemProps
),
img: ({ src, alt }: any) => {
const resolvedSrc = src && src.startsWith("/")
? `${process.env.NEXT_PUBLIC_API_URL || `${window.location.protocol}//${window.location.hostname}:8000`}${src}`
? `${process.env.NEXT_PUBLIC_API_URL || `${window.location.protocol}//${window.location.hostname}:8002`}${src}`
: src;
return (
<a href={resolvedSrc} target="_blank" rel="noopener noreferrer" className="block my-3 group">
+8 -1
View File
@@ -12,7 +12,14 @@ import type {
} from "@/types";
// API基础配置
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || (typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8000` : "http://127.0.0.1:8000");
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || (typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8002` : "http://127.0.0.1:8002");
// 将后端返回的相对路径图片URL转为完整的后端地址
export function resolveImageUrl(url: string): string {
if (!url) return url;
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("data:")) return url;
return `${API_BASE_URL}${url.startsWith("/") ? "" : "/"}${url}`;
}
// 请求拦截器
async function apiRequest<T>(