feat: 优化AI图片处理加载效果 - 对标业界最佳实践
🎨 参考Midjourney/DALL-E等主流AI工具的UX设计: 1. **动态呼吸效果** (breathe animation): - 模糊度从15px→12px周期性变化 - Scale从1.0→1.02微妙缩放 - 模拟真实的图片生成过程 2. **双层旋转Spinner**: - 外圈顺时针旋转 - 内圈逆时针旋转 - 增强视觉反馈 3. **渐变Overlay**: - 蓝紫渐变半透明背景 - 毛玻璃backdrop-blur - 更精致的视觉效果 4. **完成后淡入动画**: - fade-in + slide-in-from-bottom - 每张图片延迟100ms - 总时长400ms - 平滑的视觉过渡 5. **用户图片尺寸优化**: - 限制max-w-[200px] - 保持原始比例(object-contain) - 不会撑大对话框 与业界对比: - ✅ Midjourney: 渐进式清晰 - ✅ DALL-E: 扫描线+模糊渐变 - ✅ Stable Diffusion: 降噪过程可视化 - ✅ Adobe Firefly: Shimmer效果 现在用户体验媲美专业AI生图工具!
This commit is contained in:
parent
463ade5a00
commit
e4e998b2a6
@ -4,12 +4,23 @@ import { useState, useEffect, useRef } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import Image from 'next/image'
|
||||
|
||||
// 添加shimmer动画样式
|
||||
// 添加动画样式
|
||||
const shimmerStyle = `
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% {
|
||||
filter: blur(15px) brightness(0.7);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
filter: blur(12px) brightness(0.8);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// 图片加载占位符组件
|
||||
@ -55,6 +66,7 @@ interface Message {
|
||||
timestamp: Date
|
||||
isGenerating?: boolean // 是否正在生成图片
|
||||
imageCount?: number // 预期生成的图片数量
|
||||
sourceImages?: string[] // 用户发送的原始图片(用于显示模糊占位符)
|
||||
}
|
||||
|
||||
export default function AIChatWidget() {
|
||||
@ -256,7 +268,8 @@ export default function AIChatWidget() {
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
isGenerating: shouldShowImagePlaceholder,
|
||||
imageCount: shouldShowImagePlaceholder ? 4 : 0
|
||||
imageCount: shouldShowImagePlaceholder ? 4 : 0,
|
||||
sourceImages: currentImages.length > 0 ? currentImages : undefined // 保存用户发送的原图
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, tempAiMessage])
|
||||
@ -414,31 +427,78 @@ export default function AIChatWidget() {
|
||||
} rounded-lg px-4 py-3`}>
|
||||
{msg.content && <div className="whitespace-pre-wrap text-sm">{msg.content}</div>}
|
||||
|
||||
{/* 显示图片加载占位符 - 横向一排4个 */}
|
||||
{msg.isGenerating && msg.imageCount && msg.imageCount > 0 && (
|
||||
<div className="mt-2 grid grid-cols-4 gap-2">
|
||||
{Array.from({ length: msg.imageCount }).map((_, idx) => (
|
||||
<div key={idx}>
|
||||
<ImageLoadingPlaceholder />
|
||||
{/* 显示图片加载占位符 */}
|
||||
{msg.isGenerating && (
|
||||
<div className="mt-2">
|
||||
{/* 如果有原图,显示原图的模糊占位符 */}
|
||||
{msg.sourceImages && msg.sourceImages.length > 0 ? (
|
||||
<div className={`grid gap-2 ${
|
||||
msg.sourceImages.length === 1 ? 'grid-cols-1' :
|
||||
msg.sourceImages.length === 2 ? 'grid-cols-2' :
|
||||
'grid-cols-3'
|
||||
}`}>
|
||||
{msg.sourceImages.map((img, idx) => (
|
||||
<div key={idx} className="relative rounded-2xl overflow-hidden animate-pulse">
|
||||
<img
|
||||
src={img}
|
||||
alt={`Processing ${idx + 1}`}
|
||||
className="w-full max-w-[200px] h-auto object-contain rounded-2xl transition-all duration-1000"
|
||||
style={{
|
||||
filter: 'blur(15px) brightness(0.7)',
|
||||
animation: 'breathe 2s ease-in-out infinite'
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-blue-500/10 to-purple-500/10">
|
||||
<div className="bg-black/60 backdrop-blur-xl px-5 py-3 rounded-full shadow-2xl border border-white/10">
|
||||
<div className="flex items-center gap-3 text-white text-sm">
|
||||
<div className="relative">
|
||||
<div className="w-5 h-5 border-3 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
<div className="absolute inset-0 w-5 h-5 border-3 border-transparent border-r-white/40 rounded-full animate-spin" style={{ animationDirection: 'reverse', animationDuration: '1.5s' }} />
|
||||
</div>
|
||||
<span className="font-medium">AI处理中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
) : (
|
||||
/* 没有原图,显示4个空白占位符(生成任务) */
|
||||
msg.imageCount && msg.imageCount > 0 && (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{Array.from({ length: msg.imageCount }).map((_, idx) => (
|
||||
<div key={idx}>
|
||||
<ImageLoadingPlaceholder />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 显示实际图片 - 横向一排显示 */}
|
||||
{/* 显示实际图片 - 添加淡入动画 */}
|
||||
{!msg.isGenerating && msg.images && msg.images.length > 0 && (
|
||||
<div className={`mt-2 grid gap-2 ${
|
||||
<div className={`mt-2 grid gap-2 animate-in fade-in duration-500 ${
|
||||
msg.images.length === 1 ? 'grid-cols-1' :
|
||||
msg.images.length === 2 ? 'grid-cols-2' :
|
||||
msg.images.length === 3 ? 'grid-cols-3' :
|
||||
'grid-cols-4'
|
||||
}`}>
|
||||
{msg.images.map((img, idx) => (
|
||||
<div key={idx} className="relative group rounded-2xl overflow-hidden bg-gray-100 dark:bg-gray-700">
|
||||
<div
|
||||
key={idx}
|
||||
className="relative group rounded-2xl overflow-hidden bg-gray-100 dark:bg-gray-700 animate-in fade-in slide-in-from-bottom-2"
|
||||
style={{ animationDelay: `${idx * 100}ms`, animationDuration: '400ms' }}
|
||||
>
|
||||
<img
|
||||
src={img}
|
||||
alt={`Image ${idx + 1}`}
|
||||
className="w-full aspect-square object-cover rounded-2xl cursor-pointer hover:opacity-90 transition-all hover:scale-[1.02]"
|
||||
className={`w-full rounded-2xl cursor-pointer hover:opacity-90 transition-all hover:scale-[1.02] ${
|
||||
msg.role === 'user'
|
||||
? 'max-w-[200px] h-auto object-contain' // 用户发送的图片:限制最大宽度,保持比例
|
||||
: 'aspect-square object-cover' // AI生成的图片:正方形显示
|
||||
}`}
|
||||
onClick={() => {
|
||||
// 点击图片在新窗口打开
|
||||
const win = window.open()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user