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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>(
|
||||
|
||||
Reference in New Issue
Block a user