在Day10中,我們完成了MockData的動態渲染與各種狀態(EmptyState,Loading)的處理。然而,當你操作一個純靜態的畫面時,無論顏色配得多好看,總會有一股硬邦邦的草稿感。
微互動(Micro-interactions)與過渡動畫,正是拉開普通原型與高質感產品差距的最後10%。
今天的目標,是學習如何將人類對視覺物理感官的描述(彈性、錯落登場、微懸浮),精準轉譯為AI能識別的FramerMotion或CSSAnimation指令,為VibePulse的技能卡片與列表注入生命力!
1.視覺語言翻譯手冊:將物理感轉譯為AI能懂的詞彙
在Prompt中只寫加一點好看的動畫通常效果不好,因為AI無法判斷你想要的風格是商務沉穩還是活潑彈性。
想要精準引導AI,我們需要建立一張視覺體感與動畫參數的對照表:
2.實戰步驟1:安裝FramerMotion與設定基礎動畫Prompt
在React生態系中,FramerMotion是最受歡迎且AI掌握度最高的動畫庫。
步驟A:安裝套件
在Terminal執行:
npm install framer-motion
步驟B:指引AI建立通用FadeIn封裝組件
發送Prompt對著@components/animations/FadeIn.tsx:
請幫我用`framer-motion`建立一個可重用的動畫包覆組件`components/animations/FadeIn.tsx`。
##需求細節:
1.元件屬性(Props):
-`children`:ReactNode
-`delay`:預設0秒
-`direction`:'up'|'down'|'left'|'right'|'none'(預設'up')
-`className`:允許傳入額外Tailwind類別
2.視覺體驗:
-使用輕量平滑的easing(easeInOut,duration:0.4s)。
-位移距離不要太誇張,10px-15px即可,避免過度浮誇。
3.`'useclient'`:必須標記為ClientComponent。
實戰步驟2:升級SkillCardList.tsx的入場與懸停微互動
現在,我們讓Day10完成的SkillCardList.tsx具備:
列表開啟時卡片逐張錯落向上淡入(StaggeredFade-in-up)。
滑鼠懸停時卡片微幅上升+陰影變化(HoverLift)。
點擊時的Q彈壓下(TapBounce)。
發送重構Prompt對著@components/SkillCardList.tsx:
@components/SkillCardList.tsx
請使用`framer-motion`升級`SkillCardList.tsx`的視覺微互動與入場動畫。
##動畫需求說明:
1.容器級動畫(ContainerVariants):
-當元件載入時,發動`staggerChildren:0.08`,讓裡面的卡片依序登場。
2.卡片級動畫(CardVariants):
-Initial:opacity:0,y:15
-Animate:opacity:1,y:0
-Transition:type:"spring",stiffness:260,damping:20
-Hover體驗(`whileHover`):y:-4(微幅上浮),陰影提升(`shadow-md`),縮放`scale:1.01`。
-Press體驗(`whileTap`):`scale:0.98`(給予輕微點擊反饋)。
3.注意事項:
-保留上一版所有的語義化CSS、Badge顏色邏輯與EmptyState。
-請確保動畫不會導致排版位移(LayoutShift)。
AI產出的Framer Motion程式碼:TypeScript'use client';
import React from 'react';
import { motion, Variants } from 'framer-motion';
import { SkillItem, mockSkills } from '@/mocks/skillsData';
import { Badge } from '@/components/ui/badge';
import { BookOpen, Calendar, CheckCircle2, CircleDashed } from 'lucide-react';
// 容器 Variants (處理 stagger 錯落)
const containerVariants: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.05,
},
},
};
// 卡片 Variants (彈性物理運動)
const cardVariants: Variants = {
hidden: { opacity: 0, y: 15 },
show: {
opacity: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 260,
damping: 20,
},
},
};
const categoryStyles: Record<SkillItem['category'], string> = {
Frontend: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
Backend: 'bg-purple-500/10 text-purple-400 border-purple-500/20',
AI: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
DevOps: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
};
const getProgressColor = (val: number) => {
if (val >= 80) return 'bg-emerald-500';
if (val >= 50) return 'bg-blue-500';
return 'bg-amber-500';
};
export default function SkillCardList() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="show"
className="grid grid-cols-1 md:grid-cols-2 gap-4"
>
{mockSkills.map((skill) => (
<motion.div
key={skill.id}
variants={cardVariants}
whileHover={{ y: -4, scale: 1.01, transition: { duration: 0.2 } }}
whileTap={{ scale: 0.98 }}
className="p-5 rounded-xl bg-card border border-border text-card-foreground hover:border-primary/50 transition-colors shadow-sm hover:shadow-md flex flex-col justify-between space-y-4 cursor-pointer"
>
{/* Header */}
<div>
<div className="flex items-center justify-between gap-2 mb-2">
<span
className={`text-xs px-2.5 py-0.5 rounded-full border font-medium ${
categoryStyles[skill.category]
}`}
>
{skill.category}
</span>
{skill.status === 'mastered' ? (
<span className="flex items-center text-xs text-emerald-400 gap-1 font-medium">
<CheckCircle2 className="w-3.5 h-3.5" /> 已掌握
</span>
) : (
<span className="flex items-center text-xs text-muted-foreground gap-1">
<CircleDashed className="w-3.5 h-3.5 animate-spin-slow" /> 學習中
</span>
)}
</div>
<h3 className="font-semibold text-base line-clamp-1" title={skill.title}>
{skill.title}
</h3>
</div>
{/* Progress Section */}
<div className="space-y-1.5">
<div className="flex justify-between text-xs font-medium">
<span className="text-muted-foreground">熟練度標記</span>
<span className="text-foreground">{skill.proficiency}%</span>
</div>
<div className="w-full h-2 rounded-full bg-muted overflow-hidden">
<motion.div
className={`h-full ${getProgressColor(skill.proficiency)}`}
initial={{ width: 0 }}
animate={{ width: `${skill.proficiency}%` }}
transition={{ duration: 0.8, delay: 0.2, ease: 'easeOut' }}
/>
</div>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-1.5">
{skill.tags.map((tag) => (
<span
key={tag}
className="text-[11px] px-2 py-0.5 rounded-md bg-muted/60 text-muted-foreground"
>
#{tag}
</span>
))}
</div>
{/* Footer Metrics */}
<div className="pt-3 border-t border-border flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<BookOpen className="w-3.5 h-3.5" />
<span>{skill.notesCount} 則筆記</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="w-3.5 h-3.5" />
<span>最近練習:{new Date(skill.lastPracticedAt).toLocaleDateString()}</span>
</div>
</div>
</motion.div>
))}
</motion.div>
);
}
4.動畫與UX驗收的三大硬指標雖然微互動能大幅提升產品質感,但過度動畫(Over-animation)會帶來嚴重的視覺疲勞與效能問題。在審查AI產出的動畫時,請牢記以下三大指標:優先使用GPU加速屬性:儘量只對opacity與transform(scale,translate,rotate)做動畫。避免頻繁對width、height、margin或padding動畫,否則會觸發瀏覽器的Reflow(重排),導致低配裝置卡頓。掌控動畫時間(Duration<300ms):微互動的靈魂在於不拖泥帶水。按鈕hover或卡片浮動應控制在150ms~250ms之間;頁面入場過渡不宜超過400ms。動態移除的元件搭配AnimatePresence:如果列表卡片支援刪除或過濾,記得請AI加上,這樣卡片消失時才會展現流暢的淡出與縮小離場動畫,而不是瞬間消失。
今天我們解鎖了讓VibeCoding作品躍升為精品級MVP的關牌技能:建立了視覺體感與FramerMotion參數的自然語言翻譯表。成功為SkillCardList加上了錯落登場(Stagger)、彈性物理(Spring)與卡片hover/press微互動。掌握了動態屬性與GPU加速的效能審查原