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:
chenpei 2025-10-27 18:37:38 +08:00
parent 463ade5a00
commit e4e998b2a6

View File

@ -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()