Nano Banana AI 绘画创作前端代码(使用claude code编写)

在线

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>
    </>
  )
}
相关推荐
Ulyanov2 小时前
PyVista与Tkinter桌面级3D可视化应用实战
开发语言·前端·python·3d·信息可视化·tkinter·gui开发
计算机程序设计小李同学2 小时前
基于Web和Android的漫画阅读平台
java·前端·vue.js·spring boot·后端·uniapp
lkbhua莱克瓦242 小时前
HTML与CSS核心概念详解
前端·笔记·html·javaweb
沛沛老爹2 小时前
从Web到AI:Agent Skills CI/CD流水线集成实战指南
java·前端·人工智能·ci/cd·架构·llama·rag
GISer_Jing2 小时前
1.17-1.23日博客之星投票,每日可投
前端·人工智能·arcgis
代码游侠2 小时前
学习笔记——ARM Cortex-A 裸机开发实战指南
linux·运维·开发语言·前端·arm开发·笔记
m0_748254662 小时前
CSS 编辑器
前端·css·编辑器
Amumu121382 小时前
React扩展(二)
前端·javascript·react.js
rqtz2 小时前
网页响应式布局方法
前端·css·响应式