iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Vibe Coding

Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地系列 第 7

Day 07|畫面出來了,互動呢?用對話加入State與前端邏輯

  • 分享至 

  • xImage
  •  

在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。

畫面與狀態需求:

  1. Modal 狀態控制:

    • 接收 isOpen: booleanonClose: () => void props。
    • isOpen 為true時,顯示遮罩(Backdrop)與居中的Modal彈窗;按ESC或點擊背景可關閉。
  2. 表單欄位 (Form State):

    • 技能名稱(title: string) - Input文字框
    • 技能分類(category: 'Frontend' | 'Backend' | 'AI' | 'DevOps') - Select選單
    • 目標熟練度(proficiency: number) - Range Slider(0-100)與數值預覽
  3. 表單驗證與送出 (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串接。

雖然當前數據只存在於瀏覽器的記憶體中(重新整理後會重置),但整體產品的互動手感已經非常逼真!


上一篇
Day 06|自然語言刻UI讓AI直接幫你生成Tailwind / React組件
下一篇
Day 08|與AI一起挑選並整合UI元件庫 (Shadcn UI / Ant Design)
系列文
Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言