项目初始化与 UI 组件
约 500 字大约 2 分钟
布欧-Lewyon
2026-05-15
项目概述
全栈看板应用(Kanban Board)——支持任务管理、拖拽排序、多列看板、搜索筛选。
技术栈:React 18 + TypeScript + Vite + Zustand + React Query + React Router + dnd-kit + Vitest + Cypress。
初始化
npm create vite@latest kanban-app -- --template react-ts
cd kanban-app
# 安装核心依赖
npm install zustand @tanstack/react-query react-router-dom @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
npm install -D vitest @testing-library/react cypress项目结构
src/
├── main.tsx
├── App.tsx
├── api/ # API 请求
│ ├── client.ts # Axios 实例
│ └── boards.ts # 看板 API
├── store/ # Zustand 状态
│ └── board-store.ts # 看板 Store
├── components/ # 组件
│ ├── KanbanBoard.tsx # 看板容器
│ ├── Column.tsx # 列组件
│ ├── Card.tsx # 卡片组件
│ └── ui/ # 通用 UI
├── hooks/ # 自定义 Hook
│ ├── useBoard.ts
│ └── useDrag.ts
├── types/ # 类型
│ └── board.ts
└── pages/ # 页面
├── BoardList.tsx
└── BoardDetail.tsx类型定义
// types/board.ts
export interface Card {
id: string
title: string
description: string
priority: 'low' | 'medium' | 'high'
assignee?: string
createdAt: string
}
export interface Column {
id: string
title: string
cards: Card[]
}
export interface Board {
id: string
title: string
columns: Column[]
}UI 组件
// components/ui/Modal.tsx
interface ModalProps {
open: boolean
onClose: () => void
title: string
children: React.ReactNode
}
function Modal({ open, onClose, title, children }: ModalProps) {
if (!open) return null
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose}>×</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>,
document.body
)
}// components/ui/Select.tsx(复合组件模式)
function Select<T extends string>({ value, onChange, options, placeholder }: SelectProps<T>) {
// 实现与之前相同的复合组件模式
}dnd-kit 拖拽
import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
function KanbanBoard() {
const [activeId, setActiveId] = useState<string | null>(null)
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
)
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string)
}
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over) return
// 更新 Zustand store
boardStore.moveCard(active.id as string, over.id as string)
setActiveId(null)
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="board">
{columns.map(column => (
<ColumnContainer key={column.id} column={column} />
))}
</div>
<DragOverlay>
{activeId ? <DragCard id={activeId} /> : null}
</DragOverlay>
</DndContext>
)
}小结
- 看板应用技术栈:React + TypeScript + Zustand + React Query + dnd-kit。
- UI 组件独立封装:Modal、Select 复合组件、Portal。
- dnd-kit 实现卡片拖拽排序。
- 项目结构按功能分层(api / store / components / pages)。
下一节:状态管理与 API 集成
