Chuyển đến nội dung chính

第 15 課:Vibe 編碼全端應用 — 第 2 部分:前端

使用 React + Next.js + TailwindCSS 和 Vibe Coding 建立前端。元件產生、響應式佈局、表單處理、API 整合、狀態管理。使用人工智慧測試前端組件。

💻 程式設計 — 第 15 課 第 15 課:Vibe 編碼全端應用 — 部分 2:前端

使用 GitHub Copilot 進行 Vibe 編碼:從基礎知識到高級

第 5 部分:Vibe 編碼實踐 — 建構真實項目

亞洲開發網

1. 設定前端項目

// Prompt cho Agent Mode:
Create a Next.js 15 frontend project with:
- TypeScript, App Router
- TailwindCSS v4 with custom design tokens
- Shadcn/ui components
- Axios for API calls
- React Hook Form + Zod validation
- Zustand for state management
- Connect to the backend API at localhost:3001

代理模式自動運行 npx 創建下一個應用程式,安裝依賴項,配置TailwindCSS,以及腳手架專案結構。

2. 組件生成-Vibe Coding 的力量

2.1.佈局元件

// Prompt:
Create a responsive dashboard layout with:
- Sidebar navigation (collapsible on mobile)
- Top header with user avatar dropdown
- Main content area with breadcrumbs
- Use TailwindCSS, dark mode support
- Sidebar items: Dashboard, Projects, Tasks, Settings

Copilot 產生帶有響應斷點的完整佈局:

// src/components/layout/DashboardLayout.tsx
'use client';

import { useState } from 'react';
import { Menu, X, ChevronDown } from 'lucide-react';
import Link from 'next/link';

interface DashboardLayoutProps {
  children: React.ReactNode;
}

export function DashboardLayout({ children }: DashboardLayoutProps) {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  const navItems = [
    { href: '/dashboard', label: 'Dashboard', icon: '📊' },
    { href: '/projects', label: 'Projects', icon: '📁' },
    { href: '/tasks', label: 'Tasks', icon: '✅' },
    { href: '/settings', label: 'Settings', icon: '⚙️' },
  ];

  return (
    <div className="flex h-screen bg-gray-50 dark:bg-gray-900">
      {/* Sidebar */}
      <aside className={`
        fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-800
        transform transition-transform duration-200 ease-in-out
        ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
        lg:translate-x-0 lg:static lg:inset-0
      `}>
        <nav className="mt-8 px-4 space-y-2">
          {navItems.map((item) => (
            <Link key={item.href} href={item.href}
              className="flex items-center px-4 py-3 rounded-lg
                hover:bg-gray-100 dark:hover:bg-gray-700">
              <span className="mr-3">{item.icon}</span>
              {item.label}
            </Link>
          ))}
        </nav>
      </aside>

      {/* Main content */}
      <main className="flex-1 overflow-y-auto p-6">
        {children}
      </main>
    </div>
  );
}

2.2.任務板組件

// Prompt:
Create a Kanban board component for tasks with:
- Columns: Todo, In Progress, In Review, Done
- Drag and drop between columns (use @dnd-kit)
- Task cards showing title, priority badge, assignee avatar, due date
- Add task button per column
- Animate card movement

3. AI 表單處理

// Prompt:
Create a task creation form with:
- Title (required, max 255 chars)
- Description (optional, rich text editor)
- Priority select (Low, Medium, High, Urgent)
- Due date picker
- Assignee select (fetch from API)
- Project select
- Use React Hook Form + Zod validation
- Show inline validation errors
- Submit to POST /api/tasks
// src/components/tasks/CreateTaskForm.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { api } from '@/lib/api';

const taskSchema = z.object({
  title: z.string().min(1, 'Title is required').max(255),
  description: z.string().optional(),
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']),
  dueDate: z.string().optional(),
  assigneeId: z.string().uuid().optional(),
  projectId: z.string().uuid(),
});

type TaskFormData = z.infer<typeof taskSchema>;

export function CreateTaskForm({ projectId, onSuccess }: {
  projectId: string;
  onSuccess: () => void;
}) {
  const { register, handleSubmit, formState: { errors, isSubmitting } } =
    useForm<TaskFormData>({
      resolver: zodResolver(taskSchema),
      defaultValues: { projectId, priority: 'MEDIUM' },
    });

  const onSubmit = async (data: TaskFormData) => {
    await api.post('/tasks', data);
    onSuccess();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <input {...register('title')}
          placeholder="Task title"
          className="w-full px-4 py-2 border rounded-lg" />
        {errors.title &&
          <p className="text-red-500 text-sm mt-1">{errors.title.message}</p>}
      </div>

      <div>
        <select {...register('priority')} className="w-full px-4 py-2 border rounded-lg">
          <option value="LOW">🟢 Low</option>
          <option value="MEDIUM">🟡 Medium</option>
          <option value="HIGH">🟠 High</option>
          <option value="URGENT">🔴 Urgent</option>
        </select>
      </div>

      <button type="submit" disabled={isSubmitting}
        className="w-full bg-blue-600 text-white py-2 rounded-lg
          hover:bg-blue-700 disabled:opacity-50">
        {isSubmitting ? 'Creating...' : 'Create Task'}
      </button>
    </form>
  );
}

4.API集成

// Prompt:
Create an API client module with:
- Axios instance with base URL and auth interceptor
- Automatic token refresh on 401
- Type-safe API functions for tasks, projects, auth
- Loading and error state hooks
// src/lib/api.ts
import axios from 'axios';

export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
});

// Auth interceptor
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Auto refresh on 401
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const refreshToken = localStorage.getItem('refreshToken');
      if (refreshToken) {
        const { data } = await axios.post(
          `${api.defaults.baseURL}/auth/refresh`,
          { refreshToken }
        );
        localStorage.setItem('accessToken', data.accessToken);
        error.config.headers.Authorization = `Bearer ${data.accessToken}`;
        return api.request(error.config);
      }
    }
    return Promise.reject(error);
  }
);

5. 使用 Zustand 進行狀態管理

// Prompt:
Create a Zustand store for task management with:
- Tasks list with CRUD operations
- Filter by status, priority, assignee
- Optimistic updates for status changes
- Integration with the API client
// src/stores/taskStore.ts
import { create } from 'zustand';
import { api } from '@/lib/api';

interface Task {
  id: string;
  title: string;
  status: 'TODO' | 'IN_PROGRESS' | 'IN_REVIEW' | 'DONE';
  priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
  assignee?: { id: string; name: string; avatar?: string };
}

interface TaskStore {
  tasks: Task[];
  loading: boolean;
  fetchTasks: (projectId: string) => Promise<void>;
  updateStatus: (taskId: string, status: Task['status']) => Promise<void>;
}

export const useTaskStore = create<TaskStore>((set, get) => ({
  tasks: [],
  loading: false,

  fetchTasks: async (projectId) => {
    set({ loading: true });
    const { data } = await api.get(`/tasks?projectId=${projectId}`);
    set({ tasks: data.data, loading: false });
  },

  updateStatus: async (taskId, status) => {
    // Optimistic update
    const previousTasks = get().tasks;
    set({
      tasks: previousTasks.map((t) =>
        t.id === taskId ? { ...t, status } : t
      ),
    });

    try {
      await api.put(`/tasks/${taskId}`, { status });
    } catch {
      // Rollback on error
      set({ tasks: previousTasks });
    }
  },
}));

6.響應式設計

// Prompt:
Make the task board responsive:
- Desktop: 4-column Kanban layout
- Tablet: 2-column grid with horizontal scroll
- Mobile: single column with tab navigation for each status
- Preserve drag and drop on tablet, disable on mobile

Copilot 根據螢幕尺寸產生反應斷點和條件渲染。

7. 深色模式

// Prompt:
Add dark mode toggle with:
- System preference detection
- Manual toggle in header
- Persist preference in localStorage
- Smooth transition animation

8. 測試前端組件

// Prompt:
Write tests for CreateTaskForm component:
- Renders all form fields
- Shows validation errors for empty title
- Submits form data to API
- Disables button while submitting
- Calls onSuccess after successful submit
Use React Testing Library + Jest
// src/components/tasks/__tests__/CreateTaskForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CreateTaskForm } from '../CreateTaskForm';
import { api } from '@/lib/api';

jest.mock('@/lib/api');

describe('CreateTaskForm', () => {
  const mockOnSuccess = jest.fn();
  const projectId = 'test-project-id';

  it('shows validation error for empty title', async () => {
    render(<CreateTaskForm projectId={projectId} onSuccess={mockOnSuccess} />);

    await userEvent.click(screen.getByText('Create Task'));

    await waitFor(() => {
      expect(screen.getByText('Title is required')).toBeInTheDocument();
    });
  });

  it('submits form data correctly', async () => {
    (api.post as jest.Mock).mockResolvedValue({ data: {} });

    render(<CreateTaskForm projectId={projectId} onSuccess={mockOnSuccess} />);

    await userEvent.type(screen.getByPlaceholderText('Task title'), 'New Task');
    await userEvent.click(screen.getByText('Create Task'));

    await waitFor(() => {
      expect(api.post).toHaveBeenCalledWith('/tasks', expect.objectContaining({
        title: 'New Task',
        projectId,
        priority: 'MEDIUM',
      }));
      expect(mockOnSuccess).toHaveBeenCalled();
    });
  });
});

9. 使用 Copilot 調試前端

當遇到運行時錯誤:

// Copy error vào chat:
I'm getting this error in the browser console:
"TypeError: Cannot read properties of undefined (reading 'map')"
at TaskBoard.tsx:45

The tasks data from the API might be null on first render. Fix this with proper null checks and loading state.

Copilot 分析錯誤、尋找根本原因並修復程式碼 - 通常會新增空檢查和載入狀態。

10. 總結

任務 Vibe 編碼提示 輸出
佈局 描述佈局結構 響應式儀表板
組件 描述 UI 行為 互動組件
表格 描述字段+驗證 類型安全的表單
應用程式介面 描述端點 類型安全的客戶端
狀態 描述資料流 祖斯坦店
測試 “為 X 編寫測試” RTL 測試套件

前端 Vibe 編碼非常強大,因為 UI 元件是 模式重複很多 — AI 擅長產生重複的 UI 程式碼。

下一篇:擴展至 行動和跨平台 — 使用 Vibe 編碼回應 Native 和 Flutter。