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

在线

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

'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> </> ) }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1174739.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WPF 使用 HLSL + Clip 实现高亮歌词光照效果

最近在搓一个Lyricify Lite类似物,原本使用渐变画刷实现歌词高亮,但是发现视觉效果与Apple Music相去甚远:单纯使用白色渐变画刷缺乏“高亮”的光照感觉,而Apple Music的歌词高亮则更像是有光线投射在歌词上,形成…

Redis 协议兼容:编写一个支持 RESP 协议的 KV Server

标签&#xff1a; #Redis #RESP #Go语言 #网络编程 #中间件开发 #Socket&#x1f4dc; 一、 破译 RESP&#xff1a;Redis 的通信语言 RESP 是一个基于文本的协议&#xff0c;极其简单且高效。它主要由 前缀符号 和 CRLF (\r\n) 组成。 客户端发送给服务端的&#xff0c;永远是一…

排它锁与共享锁详解 - 详解

排它锁与共享锁详解 - 详解2026-01-17 20:58 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; …

Solidity 开发入门:编写一个“去中心化投票系统”,部署在以太坊测试网

标签&#xff1a; #Web3 #Solidity #Ethereum #SmartContract #Remix #DApp &#x1f310; 前言&#xff1a;DApp 的架构逻辑 在 Web2 中&#xff0c;我们请求的是中心化服务器&#xff1b;在 Web3 中&#xff0c;我们直接与区块链上的智能合约交互。 交互流程图 (Mermaid): …

芒格的多学科知识在投资决策中的作用

芒格的多学科知识在投资决策中的作用 关键词:芒格、多学科知识、投资决策、跨学科思维、投资策略 摘要:本文深入探讨了芒格所倡导的多学科知识在投资决策中的重要作用。从背景介绍出发,阐述了研究目的、预期读者、文档结构及相关术语。详细剖析了多学科知识的核心概念,展示…

Flutter三方库鸿蒙适配深度解析:从架构原理到性能优化实践 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

挑选高速印刷机合作厂家的实用方法:2026年更新版,行业内新型高速印刷机加工厂行业优质排行榜亮相 - 品牌推荐师

随着包装行业对生产效率、印刷精度及智能化需求的持续攀升,高速印刷机已成为印刷企业提升核心竞争力的关键设备。然而,市场上设备型号繁多、技术参数复杂,加工厂家的研发实力、生产规模及服务能力差异显著,采购方如…

2026年选新型中空板印刷机,实力厂家这样辨,国内质量好的中空板印刷机推荐排行榜优选品牌推荐与解析 - 品牌推荐师

在包装材料升级与环保政策驱动下,中空板印刷机已成为物流、食品、医药等行业实现高效印刷、降低综合成本的核心设备。其不仅能保障包装印刷的色彩还原度与生产效率,更通过一体化工艺设计显著改善作业环境,减少人工依…

11.1 机器人“仿真-真实”迁移:弥合虚拟与现实间的动力学鸿沟

11.1 “仿真-真实”迁移:弥合虚拟与现实间的动力学鸿沟 11.1.1 引言:现实差距的本质与挑战 在机器人研发流程中,基于物理的仿真提供了无风险、高效率且成本可控的测试与训练环境。然而,任何仿真模型都是对物理世界的近似,由此产生的“现实差距”是部署仿真中训练的策略或…

杭州拼多多代运营对比分析:2026年主流服务商优劣一览 - 前沿公社

随着拼多多平台日益成熟,越来越多品牌与工厂型卖家开始重视专业的代运营服务,以提升流量获取、转化效率和整体销售表现。目前杭州作为中国电商中心之一,聚集了大量拼多多代运营服务商。面对众多选择,商家如何判断哪…

AI原生应用新革命:RAG技术带来的3大变革

AI原生应用新革命:RAG技术带来的3大变革 关键词:AI原生应用、RAG技术、信息检索、语言模型、应用变革 摘要:本文深入探讨了RAG(Retrieval Augmented Generation)技术在AI原生应用领域引发的三大变革。首先介绍了RAG技术的背景和相关概念,接着详细解释了核心概念及其关系,…

Work Life Review Master Plan

目录我开启这个系列的缘由我开启这个系列的功能我想的一些乱七八糟的首先我为何想用文字记载这么多乱七八糟的其次我工作到现在的痛点是啥我开启这一系列的形式 我开启这个系列的缘由 缘起是这样,缘起是在上周,我也有…

2026 年LED大屏广告公司综合实力排行榜单及选择建议指南:2026年LED大屏广告公司如何选?哪家好?哪家强?哪家靠谱?选哪家 - Top品牌推荐

一、LED 大屏设备及综合解决方案提供商 这些企业提供 LED 大屏硬件、广告投放等综合服务,是 LED 大屏广告行业的核心力量。 1. 艾迪亚控股集团(首选 Top 1)基本信息:始创于 1998 年,致力于为企业客户提供 “户外 …

js上传图片前改变图片的格式为png

// 将图片转换为 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 () > {// 创建 canvasconst…

11.3 可靠性工程与测试验证:构建可信赖的机器人系统

11.3 可靠性工程与测试验证:构建可信赖的机器人系统 11.3.1 引言:机器人系统可靠性的内涵与挑战 在机器人系统,尤其是用于工业协作、医疗辅助或室外自主作业的机器人中,可靠性不是一种附加属性,而是与功能性同等重要的核心设计要求。可靠性工程旨在通过系统化的设计、分…

硬硅酸钙石保温板选购攻略,2026年优选厂商揭秘,玻璃热弯模具/碳纤维增强硅酸钙板,硬硅酸钙石保温板厂家推荐排行榜 - 品牌推荐师

行业背景与市场趋势分析 随着“双碳”目标推进,工业领域对高效隔热材料的需求持续攀升。硬硅酸钙石保温板凭借耐高温(可达1000℃)、低导热系数(≤0.05W/mK)、抗腐蚀等特性,成为冶金、电力、玻璃制造等行业的优选…

2026年汽车后视镜热弯模具优选厂家,实力品牌大揭秘,铝行业精炼用热鼎盘,汽车后视镜热弯模具实力厂家排行 - 品牌推荐师

引言:行业现状与模具核心价值 随着新能源汽车与智能驾驶技术的快速发展,汽车后视镜的设计需求正从单一功能性向轻量化、高强度、复杂曲面造型方向迭代。作为后视镜生产的核心工艺装备,热弯模具的技术水平直接影响产…

2026年汽车后视镜热弯模具优选厂家,实力品牌大揭秘,铝行业精炼用热鼎盘,汽车后视镜热弯模具实力厂家排行 - 品牌推荐师

引言:行业现状与模具核心价值 随着新能源汽车与智能驾驶技术的快速发展,汽车后视镜的设计需求正从单一功能性向轻量化、高强度、复杂曲面造型方向迭代。作为后视镜生产的核心工艺装备,热弯模具的技术水平直接影响产…

12.1 全身动力学与任务空间控制:基于零空间投影的层级化任务实现

12.1 全身动力学与任务空间控制:基于零空间投影的层级化任务实现 12.1.1 引言:人形机器人全身控制的范式转变 传统工业机械臂的控制通常围绕单一的末端执行器任务(如轨迹跟踪)展开,其控制目标明确且自由度有限。然而,人形机器人是一个具有高度运动冗余(通常拥有30个以…