在线
https://chat.xutongbao.top/nextjs/light/nano

javascript
'use client'
import Header from '@/components/header'
import {
ArrowLeft,
Send,
RefreshCw,
Sparkles,
Upload,
X,
Download,
Copy,
Check,
ImagePlus,
Maximize2,
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState, useEffect, useRef } from 'react'
import Image from 'next/image'
import Api from '@/api/h5Api'
interface MessageItem {
uid: string
createTime: number
info: {
message?: string
originalMessage?: string
aiMessage?: string
imgUrlCdn?: string | string[]
visionImgList?: Array<{ url: string }>
}
}
interface ApiResponse<T = any> {
code: number
message?: string
data: T
}
export default function Page() {
const router = useRouter()
const [dataSource, setDataSource] = useState<MessageItem[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isDrawing, setIsDrawing] = useState(false)
const [inputMessage, setInputMessage] = useState('')
const [current, setCurrent] = useState(1)
const [isHasMore, setIsHasMore] = useState(true)
const [copiedText, setCopiedText] = useState<string | null>(null)
const [uploadedImages, setUploadedImages] = useState<string[]>([])
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [qiniuToken, setQiniuToken] = useState<string>('')
const [isMobileDevice, setIsMobileDevice] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollContainerRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// 检测是否是移动设备/触摸设备
useEffect(() => {
const checkMobileDevice = () => {
// 检测是否支持触摸事件
const isTouchDevice =
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
(navigator as any).msMaxTouchPoints > 0
// 检测 UserAgent
const userAgent = navigator.userAgent.toLowerCase()
const isMobileUA =
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
userAgent
)
setIsMobileDevice(isTouchDevice || isMobileUA)
}
checkMobileDevice()
}, [])
// 获取七牛云上传 token
useEffect(() => {
const getUploadToken = async () => {
try {
const res = (await Api.uploadGetTokenForH5(
{}
)) as unknown as ApiResponse
if (res.code === 200 && res.data?.token) {
setQiniuToken(res.data.token)
}
} catch (error) {
console.error('获取上传token失败:', error)
}
}
getUploadToken()
}, [])
// 获取提示词
const getPrompt = (item: MessageItem) => {
if (item.info?.originalMessage) {
return item.info.originalMessage
} else if (item.info?.message) {
return item.info.message
}
return ''
}
// 加载数据
const handleSearch = async ({ page = 1, isRefresh = false } = {}) => {
if (isRefresh) {
setDataSource([])
setIsLoading(true)
}
try {
const res = (await Api.mjAppSearch({
pageNum: page,
pageSize: 10,
})) as unknown as ApiResponse
if (res.code === 200) {
const { pageNum, pageSize, total } = res.data
let list = res.data.list
if (isRefresh) {
setDataSource([...list])
} else {
setDataSource((prev) => [...prev, ...list])
}
const currentTemp = pageNum + 1
setCurrent(currentTemp)
setIsHasMore(pageNum < Math.ceil(total / pageSize))
setIsLoading(false)
}
} catch (error) {
console.error('加载失败:', error)
setIsLoading(false)
}
}
// 将图片转换为 PNG 格式
const convertImageToPng = (file: File): Promise<File> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new window.Image()
img.onload = () => {
// 创建 canvas
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
// 绘制图片到 canvas
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('无法获取 canvas context'))
return
}
ctx.drawImage(img, 0, 0)
// 转换为 PNG blob
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('图片转换失败'))
return
}
// 创建新的 File 对象
const pngFile = new File(
[blob],
file.name.replace(/\.[^.]+$/, '.png'),
{
type: 'image/png',
}
)
resolve(pngFile)
},
'image/png',
1.0
)
}
img.onerror = () => reject(new Error('图片加载失败'))
img.src = e.target?.result as string
}
reader.onerror = () => reject(new Error('文件读取失败'))
reader.readAsDataURL(file)
})
}
// 上传图片到七牛云
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0 || !qiniuToken) return
const file = files[0]
try {
// 将图片转换为 PNG 格式
const pngFile = await convertImageToPng(file)
const formData = new FormData()
const key = `ai/mjBaseImg/${Date.now()}_${pngFile.name}`
formData.append('file', pngFile)
formData.append('token', qiniuToken)
formData.append('key', key)
const response = await fetch('https://upload-z1.qiniup.com', {
method: 'POST',
body: formData,
})
const result = await response.json()
if (result.code === 200) {
const imageUrl = `https://static.xutongbao.top/${result.data.key}`
setUploadedImages((prev) => [...prev, imageUrl])
}
} catch (error) {
console.error('上传失败:', error)
}
// 清空 input,允许重复选择同一文件
e.target.value = ''
}
// 移除上传的图片
const handleRemoveImage = (index: number) => {
setUploadedImages((prev) => prev.filter((_, i) => i !== index))
}
// 发送消息
const handleSendMessage = async () => {
if (!inputMessage.trim() || isDrawing) return
setIsDrawing(true)
try {
let message = inputMessage
const res = (await Api.mjAdd({
info: { message, drawType: 'grid', type: 'imagine', uploadedImages },
})) as unknown as ApiResponse
if (res.code === 200) {
setInputMessage('')
setUploadedImages([])
await handleSearch({ page: 1, isRefresh: true })
// 滚动到顶部
setTimeout(() => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0
}
}, 100)
}
} catch (error) {
console.error('发送失败:', error)
} finally {
setIsDrawing(false)
}
}
// 复制文本
const handleCopy = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
setCopiedText(text)
setTimeout(() => setCopiedText(null), 2000)
} catch (error) {
console.error('复制失败:', error)
}
}
// 下载图片
const handleDownload = async (url: string, filename?: string) => {
try {
const response = await fetch(url)
const blob = await response.blob()
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename || `image_${Date.now()}.png`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
} catch (error) {
console.error('下载失败:', error)
}
}
// 加载更多
const handleLoadMore = () => {
if (!isLoading && isHasMore) {
handleSearch({ page: current })
}
}
// 初始化加载
useEffect(() => {
handleSearch({ page: 1, isRefresh: true })
}, [])
return (
<>
<Header />
<main className='min-h-screen bg-linear-to-br from-primary/5 via-background to-secondary/5 relative overflow-hidden'>
{/* 背景装饰 */}
<div className='absolute inset-0 overflow-hidden pointer-events-none'>
<div className='absolute top-20 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse-slow' />
<div
className='absolute bottom-20 right-1/4 w-96 h-96 bg-secondary/5 rounded-full blur-3xl animate-pulse-slow'
style={{ animationDelay: '2s' }}
/>
<div
className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-accent/5 rounded-full blur-3xl animate-pulse-slow'
style={{ animationDelay: '4s' }}
/>
</div>
{/* 内容区域 */}
<div className='relative max-w-6xl mx-auto px-3 sm:px-4 py-3 sm:py-6 h-screen flex flex-col gap-3 sm:gap-4'>
{/* 头部:返回按钮和刷新按钮 */}
<div className='flex items-center justify-between animate-fade-in'>
<button
onClick={() => router.push('/light')}
className='group flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg sm:rounded-xl bg-card/60 backdrop-blur-xl border border-border/50 hover:border-primary/50 shadow-sm hover:shadow-md transition-all duration-300'
>
<ArrowLeft className='w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors duration-300 group-hover:-translate-x-0.5' />
<span className='text-xs sm:text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors duration-300'>
返回
</span>
</button>
<button
onClick={() => handleSearch({ page: 1, isRefresh: true })}
disabled={isLoading}
className='group flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg sm:rounded-xl bg-card/60 backdrop-blur-xl border border-border/50 hover:border-primary/50 shadow-sm hover:shadow-md transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed'
>
<RefreshCw
className={`w-4 h-4 text-muted-foreground group-hover:text-primary transition-all duration-300 ${
isLoading ? 'animate-spin' : 'group-hover:rotate-180'
}`}
/>
<span className='text-xs sm:text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors duration-300'>
刷新
</span>
</button>
</div>
{/* 创作输入区域 - 放在顶部,突出显示 */}
<div
className='bg-card/90 backdrop-blur-2xl rounded-2xl sm:rounded-3xl border-2 border-primary/20 shadow-2xl shadow-primary/10 p-3 sm:p-6 animate-fade-in-up relative overflow-hidden'
style={{ animationDelay: '0.1s' }}
>
{/* 装饰性背景 */}
<div className='absolute top-0 right-0 w-64 h-64 bg-linear-to-br from-primary/10 to-transparent rounded-full blur-3xl pointer-events-none' />
<div className='absolute bottom-0 left-0 w-64 h-64 bg-linear-to-tr from-secondary/10 to-transparent rounded-full blur-3xl pointer-events-none' />
<div className='relative space-y-3 sm:space-y-4'>
{/* 标题 */}
<div className='flex items-center gap-2 sm:gap-3 mb-1 sm:mb-2'>
<div className='flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-xl sm:rounded-2xl bg-linear-to-br from-primary to-secondary shadow-lg'>
<Sparkles className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground animate-pulse' />
</div>
<div>
<h2 className='text-base sm:text-xl font-bold bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent'>
Nano Banana AI 绘画创作
</h2>
<p className='text-xs text-muted-foreground hidden sm:block'>
描述您的创意,AI 为您创作精美图像
</p>
</div>
</div>
{/* 上传图片区域 */}
{uploadedImages.length > 0 && (
<div className='flex flex-wrap gap-1.5 sm:gap-2'>
{uploadedImages.map((img, index) => (
<div
key={index}
className='relative group w-16 h-16 sm:w-20 sm:h-20 rounded-lg sm:rounded-xl overflow-hidden border-2 border-border hover:border-primary transition-all duration-300'
>
<Image
src={img}
alt={`上传 ${index + 1}`}
fill
className='object-cover'
/>
<button
onClick={() => handleRemoveImage(index)}
className={`absolute top-1 right-1 w-5 h-5 rounded-full bg-destructive/80 backdrop-blur-sm flex items-center justify-center transition-opacity duration-300 ${
isMobileDevice
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100'
}`}
>
<X className='w-3 h-3 text-destructive-foreground' />
</button>
</div>
))}
</div>
)}
{/* 输入框 */}
<div className='relative'>
<textarea
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}}
placeholder='描述您想要创作的图像...'
disabled={isDrawing}
className='w-full px-3 sm:px-4 py-2.5 sm:py-3 rounded-xl sm:rounded-2xl bg-background/50 border-2 border-border focus:border-primary focus:outline-none resize-none transition-all duration-300 text-sm sm:text-base text-foreground placeholder:text-muted-foreground disabled:opacity-50 disabled:cursor-not-allowed min-h-[80px] sm:min-h-[100px]'
rows={3}
maxLength={2000}
/>
<div className='absolute bottom-2 sm:bottom-3 right-2 sm:right-3 text-xs text-muted-foreground bg-background/80 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded'>
{inputMessage.length}/2000
</div>
</div>
{/* 操作按钮 */}
<div className='flex items-center gap-2 sm:gap-3'>
<input
ref={fileInputRef}
type='file'
accept='image/*'
onChange={handleImageUpload}
className='hidden'
multiple={false}
/>
<button
onClick={() => fileInputRef.current?.click()}
disabled={isDrawing || uploadedImages.length >= 3}
className='flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg sm:rounded-xl bg-muted/50 hover:bg-muted border border-border hover:border-primary/50 text-muted-foreground hover:text-foreground transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed'
>
<ImagePlus className='w-4 h-4' />
<span className='text-xs sm:text-sm font-medium hidden xs:inline'>上传</span>
{uploadedImages.length > 0 && (
<span className='text-xs bg-primary/20 text-primary px-1.5 sm:px-2 py-0.5 rounded-full'>
{uploadedImages.length}/3
</span>
)}
</button>
<div className='flex-1' />
<button
onClick={handleSendMessage}
disabled={
(!inputMessage.trim() && uploadedImages.length === 0) ||
isDrawing
}
className='group relative overflow-hidden rounded-xl sm:rounded-2xl bg-linear-to-r from-primary to-secondary p-0.5 hover:shadow-xl hover:shadow-primary/25 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none hover:scale-105'
>
<div className='relative bg-card/90 backdrop-blur-sm rounded-xl sm:rounded-2xl px-4 sm:px-8 py-2 sm:py-3 group-hover:bg-transparent transition-all duration-300'>
<div className='flex items-center gap-1.5 sm:gap-2 text-foreground group-hover:text-primary-foreground transition-colors duration-300'>
{isDrawing ? (
<>
<RefreshCw className='w-4 h-4 sm:w-5 sm:h-5 animate-spin' />
<span className='text-xs sm:text-base font-semibold'>创作中...</span>
</>
) : (
<>
<Send className='w-4 h-4 sm:w-5 sm:h-5' />
<span className='text-xs sm:text-base font-semibold'>开始创作</span>
</>
)}
</div>
</div>
</button>
</div>
</div>
</div>
{/* 作品列表区域 - 低调展示 */}
<div className='flex-1 overflow-hidden flex flex-col'>
<div className='text-xs text-muted-foreground mb-1.5 sm:mb-2 px-1'>
创作历史
</div>
<div
ref={scrollContainerRef}
className='flex-1 overflow-y-auto space-y-2 sm:space-y-3 pr-1 sm:pr-2 custom-scrollbar animate-fade-in-up'
style={{ animationDelay: '0.2s' }}
>
{dataSource.map((item, index) => (
<div
key={item.uid}
className='group bg-card/40 backdrop-blur-sm rounded-xl sm:rounded-2xl border border-border/50 hover:border-border hover:bg-card/60 shadow-sm hover:shadow-md transition-all duration-300 p-2.5 sm:p-4'
>
<div className='flex gap-2 sm:gap-3'>
{/* 图片区域 */}
{(item.info.imgUrlCdn || item.info.visionImgList) && (
<div className='flex-shrink-0'>
{/* Vision 图片列表 */}
{Array.isArray(item.info.visionImgList) &&
item.info.visionImgList.length > 0 && (
<div className='grid grid-cols-2 gap-1.5 sm:gap-2'>
{item.info.visionImgList.map((img, imgIndex) => (
<div
key={imgIndex}
className='relative w-16 h-16 sm:w-24 sm:h-24 rounded-lg sm:rounded-xl overflow-hidden border border-border hover:border-primary transition-all duration-300 cursor-pointer group/img'
onClick={() => setPreviewImage(img.url)}
>
<Image
src={img.url}
alt={`Vision ${imgIndex + 1}`}
fill
className='object-cover'
/>
<div className='absolute inset-0 bg-black/0 group-hover/img:bg-black/40 transition-colors duration-300 flex items-center justify-center'>
<Maximize2 className='w-6 h-6 text-white opacity-0 group-hover/img:opacity-100 transition-opacity duration-300' />
</div>
{/* 操作按钮 - 移动端禁用点击避免阻挡预览 */}
<div className='absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-300 pointer-events-none sm:pointer-events-auto'>
<button
onClick={(e) => {
e.stopPropagation()
handleDownload(img.url)
}}
className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
<Download className='w-3 h-3 text-primary-foreground' />
</button>
<button
onClick={(e) => {
e.stopPropagation()
handleCopy(img.url)
}}
className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
{copiedText === img.url ? (
<Check className='w-3 h-3 text-primary-foreground' />
) : (
<Copy className='w-3 h-3 text-primary-foreground' />
)}
</button>
</div>
</div>
))}
</div>
)}
{/* CDN 图片 */}
{Array.isArray(item.info.imgUrlCdn) ? (
<div className='grid grid-cols-2 gap-1.5 sm:gap-2'>
{item.info.imgUrlCdn.map((url, imgIndex) => (
<div
key={imgIndex}
className='relative w-16 h-16 sm:w-24 sm:h-24 rounded-lg sm:rounded-xl overflow-hidden border border-border hover:border-primary transition-all duration-300 cursor-pointer group/img'
onClick={() => setPreviewImage(url)}
>
<Image
src={url}
alt={`图片 ${imgIndex + 1}`}
fill
className='object-cover'
/>
<div className='absolute inset-0 bg-black/0 group-hover/img:bg-black/40 transition-colors duration-300 flex items-center justify-center'>
<Maximize2 className='w-6 h-6 text-white opacity-0 group-hover/img:opacity-100 transition-opacity duration-300' />
</div>
<div className='absolute top-1 right-1 bg-black/60 backdrop-blur-sm text-white text-xs px-2 py-0.5 rounded-lg'>
{imgIndex + 1}
</div>
{/* 操作按钮 - 移动端禁用点击避免阻挡预览 */}
<div className='absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-300 pointer-events-none sm:pointer-events-auto'>
<button
onClick={(e) => {
e.stopPropagation()
handleDownload(url)
}}
className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
<Download className='w-3 h-3 text-primary-foreground' />
</button>
<button
onClick={(e) => {
e.stopPropagation()
handleCopy(url)
}}
className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
{copiedText === url ? (
<Check className='w-3 h-3 text-primary-foreground' />
) : (
<Copy className='w-3 h-3 text-primary-foreground' />
)}
</button>
</div>
</div>
))}
</div>
) : item.info.imgUrlCdn ? (
<div
className='relative w-20 h-20 sm:w-28 sm:h-28 rounded-lg sm:rounded-xl overflow-hidden border border-border hover:border-primary transition-all duration-300 cursor-pointer group/img'
onClick={() =>
setPreviewImage(item.info.imgUrlCdn as string)
}
>
<Image
src={item.info.imgUrlCdn}
alt='图片'
fill
className='object-cover'
/>
<div className='absolute inset-0 bg-black/0 group-hover/img:bg-black/40 transition-colors duration-300 flex items-center justify-center'>
<Maximize2 className='w-6 h-6 text-white opacity-0 group-hover/img:opacity-100 transition-opacity duration-300' />
</div>
{/* 操作按钮 - 移动端禁用点击避免阻挡预览 */}
<div className='absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-300 pointer-events-none sm:pointer-events-auto'>
<button
onClick={(e) => {
e.stopPropagation()
handleDownload(item.info.imgUrlCdn as string)
}}
className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
<Download className='w-3 h-3 text-primary-foreground' />
</button>
<button
onClick={(e) => {
e.stopPropagation()
handleCopy(item.info.imgUrlCdn as string)
}}
className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
{copiedText === item.info.imgUrlCdn ? (
<Check className='w-3 h-3 text-primary-foreground' />
) : (
<Copy className='w-3 h-3 text-primary-foreground' />
)}
</button>
</div>
</div>
) : null}
</div>
)}
{/* 文本内容区域 */}
<div className='flex-1 min-w-0 space-y-1.5 sm:space-y-2'>
{/* 提示词 */}
{getPrompt(item) && (
<div className='space-y-0.5 sm:space-y-1'>
<div className='text-xs text-muted-foreground'>
提示词
</div>
<p className='text-xs sm:text-sm text-foreground/80 leading-relaxed line-clamp-2'>
{getPrompt(item)}
</p>
<button
onClick={() => handleCopy(getPrompt(item))}
className='text-xs text-muted-foreground hover:text-foreground transition-colors duration-300 flex items-center gap-1'
>
{copiedText === getPrompt(item) ? (
<>
<Check className='w-3 h-3' />
<span>已复制</span>
</>
) : (
<>
<Copy className='w-3 h-3' />
<span>复制</span>
</>
)}
</button>
</div>
)}
{/* AI 回复 */}
{item.info?.aiMessage && (
<div className='space-y-0.5 sm:space-y-1'>
<div className='text-xs text-muted-foreground flex items-center gap-1'>
<Sparkles className='w-3 h-3' />
AI 回复
</div>
<p className='text-xs sm:text-sm text-foreground/60 leading-relaxed line-clamp-2'>
{item.info.aiMessage}
</p>
</div>
)}
{/* 时间戳 */}
<div className='text-xs text-muted-foreground'>
{new Date(Number(item.createTime)).toLocaleString(
'zh-CN',
{
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}
)}
</div>
</div>
</div>
</div>
))}
{/* 加载更多按钮 */}
{isHasMore && dataSource.length > 0 && (
<div className='flex justify-center py-3'>
<button
onClick={handleLoadMore}
disabled={isLoading}
className='flex items-center gap-2 px-4 py-2 rounded-xl bg-card/60 backdrop-blur-sm border border-border/50 hover:border-border text-muted-foreground hover:text-foreground transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed text-sm'
>
<RefreshCw
className={`w-4 h-4 transition-transform duration-300 ${
isLoading ? 'animate-spin' : ''
}`}
/>
<span>{isLoading ? '加载中...' : '加载更多'}</span>
</button>
</div>
)}
{/* 空状态 */}
{dataSource.length === 0 && !isLoading && (
<div className='flex flex-col items-center justify-center h-full space-y-3 opacity-40'>
<Sparkles className='w-12 h-12 text-muted-foreground' />
<p className='text-muted-foreground text-sm'>暂无创作历史</p>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
</div>
</main>
{/* 图片预览弹窗 */}
{previewImage && (
<div
className='fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-2 sm:p-4 animate-fade-in'
onClick={() => setPreviewImage(null)}
>
<div
className='relative max-w-4xl max-h-[90vh] w-full'
onClick={(e) => e.stopPropagation()}
>
<Image
src={previewImage}
alt='预览'
width={1200}
height={1200}
className='rounded-xl sm:rounded-2xl object-contain max-h-[85vh] w-full'
/>
<div className='absolute top-2 sm:top-4 right-2 sm:right-4 flex gap-1.5 sm:gap-2'>
<button
onClick={() => handleDownload(previewImage)}
className='w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
<Download className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground' />
</button>
<button
onClick={() => handleCopy(previewImage)}
className='w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300'
>
{copiedText === previewImage ? (
<Check className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground' />
) : (
<Copy className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground' />
)}
</button>
<button
onClick={() => setPreviewImage(null)}
className='w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl bg-destructive/90 backdrop-blur-sm flex items-center justify-center hover:bg-destructive transition-colors duration-300'
>
<X className='w-4 h-4 sm:w-5 sm:h-5 text-destructive-foreground' />
</button>
</div>
</div>
</div>
)}
<style jsx global>{`
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
@media (min-width: 640px) {
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--muted));
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.5);
}
`}</style>
</>
)
}