module5
90 分钟

前端页面构建

学习进度

当前模块0/6 课
总进度0/36 课
学习时长0 分钟

学习目标

  • 用 Codex 生成组件和页面代码
  • 实现组件化开发和状态管理
  • 快速构建响应式布局
  • 掌握 Cursor + Codex 协作工作流

403 前端页面构建

使用 Cursor + Codex 组合拳,快速构建企业级前端界面

学习目标

完成本课程后,你将能够:

一、前端开发工具组合

Cursor + Codex 协作模式

场景使用工具说明
页面/组件生成Codex对话式生成完整代码
局部编辑调试CursorTab 补全 + 内联编辑
样式调整Cursor实时预览修改
重构优化Codex`/refactor` 批量重构
调试排错Codex`/debug` 错误分析

开发流程

1. Codex 生成页面骨架和组件代码  →  2. Cursor 精调 UI 细节
3. Codex 审核代码质量            →  4. Cursor 修复 lint 问题
5. Codex 生成测试                →  6. 浏览器验证

二、任务看板页面

用 Codex 生成主看板

请为任务管理平台生成看板页面,使用 Next.js 14 App Router + Tailwind CSS:
- 四列看板:待办、进行中、审核中、已完成
- 每列显示任务卡片,支持拖拽排序
- 任务卡片显示标题、优先级标签、指派人头像、截止日期
- 顶部工具栏:搜索框、筛选器、新建任务按钮
- 响应式布局,移动端折叠为单列滚动

页面骨架

// frontend/src/app/dashboard/page.tsx
'use client';

import { useState } from 'react';
import { BoardColumn } from '@/components/board/BoardColumn';
import { TaskCard } from '@/components/task/TaskCard';
import { SearchBar } from '@/components/board/SearchBar';
import { FilterBar } from '@/components/board/FilterBar';
import { CreateTaskModal } from '@/components/task/CreateTaskModal';
import { useBoard } from '@/hooks/useBoard';

export default function DashboardPage() {
  const {
    columns,
    filters,
    setFilters,
    handleDragEnd,
    isLoading,
    error,
  } = useBoard();

  if (error) return <ErrorState message={error} />;

  return (
    <div className="h-screen flex flex-col bg-gray-50">
      {/* 顶部工具栏 */}
      <header className="bg-white border-b px-6 py-4">
        <div className="flex items-center justify-between mb-4">
          <h1 className="text-2xl font-bold">任务看板</h1>
          <CreateTaskButton />
        </div>
        <div className="flex gap-4">
          <SearchBar onSearch={setFilters} />
          <FilterBar filters={filters} onChange={setFilters} />
        </div>
      </header>

      {/* 看板主体 */}
      <main className="flex-1 overflow-x-auto p-6">
        {isLoading ? (
          <BoardSkeleton />
        ) : (
          <div className="flex gap-4 min-h-full">
            {columns.map((column) => (
              <BoardColumn
                key={column.id}
                column={column}
                onDragEnd={handleDragEnd}
              />
            ))}
          </div>
        )}
      </main>
    </div>
  );
}

三、组件化开发

任务卡片组件

// frontend/src/components/task/TaskCard.tsx
'use client';

import { useDraggable } from '@dnd-kit/core';
import { Badge } from '@/components/ui/Badge';
import { Avatar } from '@/components/ui/Avatar';
import { formatDate, isOverdue } from '@/lib/utils';

interface TaskCardProps {
  task: Task;
  isDragging?: boolean;
}

export function TaskCard({ task, isDragging }: TaskCardProps) {
  const { attributes, listeners, setNodeRef, transform } = useDraggable({
    id: task.id,
  });

  const style = transform ? {
    transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
  } : undefined;

  const priorityColors: Record<Priority, string> = {
    URGENT: 'bg-red-100 text-red-700',
    HIGH: 'bg-orange-100 text-orange-700',
    MEDIUM: 'bg-blue-100 text-blue-700',
    LOW: 'bg-gray-100 text-gray-700',
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      {...listeners}
      {...attributes}
      className={`
        bg-white rounded-lg p-4 shadow-sm border cursor-grab
        hover:shadow-md transition-shadow
        ${isDragging ? 'opacity-50 shadow-lg' : ''}
      `}
    >
      {/* 优先级 + 标签 */}
      <div className="flex items-center gap-2 mb-2">
        <Badge className={priorityColors[task.priority]}>
          {task.priority}
        </Badge>
        {task.tags?.map((tag) => (
          <Badge key={tag.id} style={{ backgroundColor: tag.color }}>
            {tag.name}
          </Badge>
        ))}
      </div>

      {/* 标题 */}
      <h3 className="font-medium text-gray-900 mb-2 line-clamp-2">
        {task.title}
      </h3>

      {/* 底部信息 */}
      <div className="flex items-center justify-between mt-4">
        <div className="flex items-center gap-2">
          {task.assignee && (
            <Avatar
              src={task.assignee.avatar}
              name={task.assignee.name}
              size="sm"
            />
          )}
        </div>
        {task.dueDate && (
          <span className={`text-xs ${isOverdue(task.dueDate) ? 'text-red-500' : 'text-gray-500'}`}>
            {formatDate(task.dueDate)}
          </span>
        )}
      </div>
    </div>
  );
}

UI 基础组件库

用 Codex 快速生成常用 UI 组件:

请为项目生成一套基础 UI 组件:
- Button(变体:primary/secondary/ghost/danger)
- Input / Textarea(带错误状态)
- Modal / Dialog(带遮罩动画)
- Badge / Tag
- Avatar(支持图片和首字母回退)
- Skeleton(加载骨架屏)
- Toast(通知提示)

使用 Tailwind CSS + Radix UI,确保无障碍访问

四、状态管理

数据获取 Hook

// frontend/src/hooks/useBoard.ts
import useSWR from 'swr';
import { useState, useCallback } from 'react';

const fetcher = (url: string) => fetch(url).then(r => r.json());

export function useBoard() {
  const [filters, setFilters] = useState<BoardFilters>({
    search: '',
    priority: '',
    assigneeId: '',
  });

  const queryString = new URLSearchParams(
    Object.entries(filters).filter(([_, v]) => v)
  ).toString();

  const { data, error, isLoading, mutate } = useSWR(
    `/api/tasks/board?${queryString}`,
    fetcher,
    { refreshInterval: 30000 } // 30秒轮询
  );

  const handleDragEnd = useCallback(async (result: DragEndEvent) => {
    const { active, over } = result;
    if (!over) return;

    // 乐观更新
    await mutate(optimisticUpdate(active.id, over.id), false);
  }, [mutate]);

  return {
    columns: data?.columns ?? [],
    filters,
    setFilters,
    handleDragEnd,
    isLoading,
    error,
    mutate,
  };
}

状态管理策略

数据类型方案工具
服务端数据请求缓存SWR / React Query
全局 UI 状态Context + ReducerReact Context
表单状态受控组件React Hook Form + Zod
URL 参数路由查询参数next/navigation

五、响应式设计

Tailwind 响应式断点

// 使用 Tailwind 响应式前缀
<div className="
  grid
  grid-cols-1          // 移动端:单列
  md:grid-cols-2       // 平板:两列
  lg:grid-cols-4       // 桌面:四列
  gap-4
  p-4 md:p-6 lg:p-8
">

响应式 Hook

// frontend/src/hooks/useMediaQuery.ts
import { useEffect, useState } from 'react';

export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    setMatches(media.matches);

    const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
    media.addEventListener('change', listener);
    return () => media.removeEventListener('change', listener);
  }, [query]);

  return matches;
}

// 使用
const isMobile = useMediaQuery('(max-width: 768px)');

六、实战练习

练习 1:生成表单页面

请为任务创建/编辑生成一个完整表单页面,包含:
- 标题、描述、优先级、截止日期
- 所属项目和指派人的下拉选择
- 标签多选
- React Hook Form + Zod 验证
- 加载和错误状态处理

练习 2:生成数据表格

请生成一个项目列表页面,包含:
- 服务端分页的数据表格
- 搜索、排序、筛选
- 行内编辑
- 批量操作
- 导出功能

练习 3:错误处理

请为所有 API 调用添加统一的错误处理:
- 网络错误重试(最多 3 次)
- 401 自动跳转登录
- 错误 Toast 提示
- 全局 ErrorBoundary

七、本课总结

**核心要点:**

  • Cursor 负责精调,Codex 负责生成和重构,两者互补
  • 组件按功能拆分,保持单一职责
  • SWR / React Query 管理服务端状态,避免手动管理 loading/error
  • Tailwind 响应式前缀 + useMediaQuery Hook 覆盖多端适配
  • 乐观更新提升体验,数据一致性靠 mutate 回调保障

**下一步:** 模块 404 — 数据库设计与优化

下一模块