在Day 06中,我們運用自然語言快速拉出了VibePulse的靜態Dashboard畫面與數據統計卡片。
但是,一個只有靜態HTML/Tailwind視覺的畫面充其量只是個Wireframe。要讓產品真正活過來,必須注入狀態管理(State Management)、使用者互動事件(Event Handling)與表單驗證(Form Validation)。
今天,我們將示範如何透過Ping-Pong對話,為VibePulse加上新增技能(Add Skill Modal)的完整前端動態互動邏輯。
1.前端互動開發的Vibe節奏:先狀態,後互動
當我們要請AI為元件注入互動邏輯時,直接喊幫我做一個可以新增技能的功能容易讓AI寫出狀態混亂的程式碼。
建議將互動任務拆解為兩個層次:
定義State與Data Interface:先明確UI隨時間變化的狀態變數。
實作Event Handlers與邊界驗證:觸發動作(Modal開關、Submit提交、格式檢查)。
2.實戰演練:打造動態新增技能Modal與表單驗證
我們預計實作:
點擊+新增技能按鈕,彈出Dialog Modal。
表單欄位:技能名稱(Title)、類別(Category Dropdown)、目標熟練度(Target Proficiency 0-100%)。
表單驗證:標題不得為空,熟練度必須在0~100之間。
送出後自動更新Dashboard中的技能列表State。
步驟1:發送狀態與Modal需求Prompt
開啟Cursor,對著@components/AddSkillModal.tsx發送Prompt:
@types/article.ts (請參考基礎型別)
請幫我建立一個可重用的 React 彈出視窗元件 AddSkillModal.tsx,使用 Tailwind CSS 與 Lucide Icons。
Modal 狀態控制:
isOpen: boolean 與 onClose: () => void props。isOpen 為true時,顯示遮罩(Backdrop)與居中的Modal彈窗;按ESC或點擊背景可關閉。表單欄位 (Form State):
title: string) - Input文字框category: 'Frontend' | 'Backend' | 'AI' | 'DevOps') - Select選單proficiency: number) - Range Slider(0-100)與數值預覽表單驗證與送出 (Validation & Submit):
onSubmit: (newSkill: SkillInput) => void。title除去空白後為空,顯示紅字錯誤訊息技能名稱不可為空。onSubmit 並清空表單、關閉Modal。AI產出的互動Component程式碼
AI生成了包含完整的React State、Keyboard Event與Validation邏輯的元件:
import React, { useState, useEffect } from 'react';
import { X } from 'lucide-react';
export interface SkillInput {
title: string;
category: 'Frontend' | 'Backend' | 'AI' | 'DevOps';
proficiency: number;
}
interface AddSkillModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (skill: SkillInput) => void;
}
export default function AddSkillModal({ isOpen, onClose, onSubmit }: AddSkillModalProps) {
const [title, setTitle] = useState('');
const [category, setCategory] = useState<SkillInput['category']>('Frontend');
const [proficiency, setProficiency] = useState(50);
const [error, setError] = useState('');
// 按下 ESC 鍵關閉 Modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) {
setError('請輸入技能名稱!');
return;
}
setError('');
onSubmit({ title: title.trim(), category, proficiency });
// 重置表單
setTitle('');
setProficiency(50);
onClose();
};
return (
新增學習技能
<form onSubmit={handleSubmit} className="mt-4 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-1">技能名稱</label>
<input
type="text"
value={title}
onChange={(e) => {
setTitle(e.target.value);
if (error) setError('');
}}
placeholder="例如:Next.js App Router"
className="w-full px-3 py-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{error && <p className="mt-1 text-xs text-rose-400">{error}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-1">技能分類</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value as SkillInput['category'])}
className="w-full px-3 py-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="Frontend">Frontend</option>
<option value="Backend">Backend</option>
<option value="AI">AI</option>
<option value="DevOps">DevOps</option>
</select>
</div>
<div>
<div className="flex justify-between text-sm font-medium text-slate-300 mb-1">
<span>當前掌握度</span>
<span className="text-blue-400">{proficiency}%</span>
</div>
<input
type="range"
min="0"
max="100"
value={proficiency}
onChange={(e) => setProficiency(Number(e.target.value))}
className="w-full accent-blue-500 cursor-pointer"
/>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-slate-800">
<button
type="button"
onClick={onClose}
className="px-4 py-2 rounded-lg text-sm text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition"
>
取消
</button>
<button
type="submit"
className="px-4 py-2 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-500/20 transition"
>
確認新增
</button>
</div>
</form>
</div>
</div>
);
}
步驟2:在主頁面綁定動態State
回到主頁面@app/page.tsx,請AI將AddSkillModal與主頁面的狀態綁定:
Prompt:@app/page.tsx請引入AddSkillModal,並建立skills清單的React State(初始包含3項假資料)。畫面上方加入一個+新增技能按鈕,點擊時開啟Modal。當Modal提交時,將新技能動態prepend append到skills列表中。
4.前端邏輯驗收與常見陷阱
在Vibe Coding驗收互動邏輯時,請特別留意以下三大前端常犯Bug:
State異動未被React感知(Mutation陷阱):
檢查AI是否寫出skills.push(newSkill)(壞習慣!)。應確保使用不變性寫法setSkills([newSkill, ...skills])。
Form預設行為干擾:
點擊按鈕時頁面是否意外重新整理?檢查onSubmit是否記得調用e.preventDefault()。
Modal關閉後State未重置:
再次開啟Modal時,上次輸入的暫存值或錯誤訊息是否還殘留在畫面上?
今天我們成功讓VibePulse從靜態畫面跨越到了可動態操作的原型階段:
利用對話完成了表單狀態(Controlled Components)維護。
建立了表單驗證與Modal事件處理。
實現了主畫面數據與Modal子組件之間的State串接。
雖然當前數據只存在於瀏覽器的記憶體中(重新整理後會重置),但整體產品的互動手感已經非常逼真!