React+Zustand+Vite实战:构建可拖拽任务看板与数据可视化应用
在开发过程中我们常常会遇到需要处理复杂业务逻辑、管理组件状态或实现特定动画效果的需求。尤其是在构建中后台系统、数据可视化大屏或交互式应用时如何优雅地组织代码、实现流畅的交互并确保应用的可维护性是每个开发者都会面临的挑战。本文将围绕一个综合性前端实战项目深入拆解从项目初始化、核心功能实现到性能优化与部署上线的完整闭环。无论你是希望巩固现代前端技术栈的开发者还是正在寻找一个完整项目练手的新手都能从本文中获得一套可直接复用的解决方案。1. 项目背景与核心概念本次实战项目我们将构建一个名为“智能任务看板”的单页面应用SPA。它模拟了一个轻量级的项目协作工具核心功能包括任务的增删改查、状态拖拽切换、数据可视化统计以及用户偏好设置。通过这个项目我们将串联起以下关键技术点前端框架与状态管理使用 React 作为UI库并搭配 Zustand 进行轻量级状态管理避免 Props Drilling 的繁琐。构建工具与开发体验使用 Vite 作为构建工具享受极速的热更新和优化的构建输出。UI 组件与样式方案采用 Ant Design 作为基础UI组件库并结合 CSS Modules 或 Styled-components 进行组件级样式封装保证样式隔离。路由管理使用 React Router v6 管理应用内的页面导航。数据可视化集成 ECharts 来实现任务完成情况的统计图表。交互与动画使用dnd-kit库实现任务的拖拽排序与状态转移并添加适当的过渡动画提升用户体验。工程化与优化涵盖代码规范ESLint, Prettier、打包优化、环境变量配置等生产级实践。这个项目涵盖了现代前端开发中的大部分核心环节理解其实现过程能帮助你系统性地掌握如何将一个产品需求转化为可维护、高性能的代码。2. 环境准备与版本说明在开始编码之前请确保你的本地开发环境已就绪。以下是本文示例所使用的主要技术版本你可以根据实际情况进行调整但建议使用 LTS 或稳定版本以避免兼容性问题。操作系统Windows 10/11, macOS, 或 Linux (本文命令以 macOS/Linux 为例)Node.js 18.0.0 (推荐使用 nvm 或 fnm 进行版本管理)包管理器npm (随 Node.js 安装) 或 yarn / pnpm (推荐 pnpm速度更快)浏览器Chrome / Edge / Firefox 最新版首先我们可以使用 Vite 官方模板快速初始化项目。打开终端执行以下命令# 使用 npm npm create vitelatest smart-task-board -- --template react-ts # 或使用 pnpm pnpm create vite smart-task-board --template react-ts # 进入项目目录 cd smart-task-board创建完成后项目结构大致如下smart-task-board/ ├── index.html ├── package.json ├── vite.config.ts ├── tsconfig.json ├── public/ └── src/ ├── main.tsx ├── App.tsx ├── App.css └── vite-env.d.ts接下来安装项目所需的主要依赖库# 使用 pnpm 安装推荐 pnpm add react-router-dom zustand antd ant-design/icons echarts echarts-for-react pnpm add -D types/node dnd-kit/sortable dnd-kit/core dnd-kit/utilities dnd-kit/components # 或使用 npm npm install react-router-dom zustand antd ant-design/icons echarts echarts-for-react npm install -D types/node dnd-kit/sortable dnd-kit/core dnd-kit/utilities dnd-kit/components安装完成后建议配置代码格式化工具。在项目根目录创建.eslintrc.cjs和.prettierrc文件以统一团队代码风格具体配置可参考各工具官方文档。3. 核心架构与状态设计在编写具体页面之前良好的架构设计是项目成功的基石。我们将采用“功能切片Feature Slice”的思想来组织代码并将应用状态集中管理。3.1 项目目录结构规划我们规划一个清晰且可扩展的目录结构src/ ├── main.tsx ├── App.tsx ├── assets/ # 静态资源 ├── components/ # 通用组件 (Button, Modal等) │ ├── common/ # 纯UI组件 │ └── charts/ # 图表封装组件 ├── features/ # 功能模块 │ ├── taskBoard/ # 任务看板模块 │ ├── dashboard/ # 数据仪表盘模块 │ └── settings/ # 用户设置模块 ├── stores/ # 状态管理 (Zustand stores) ├── hooks/ # 自定义 React Hooks ├── utils/ # 工具函数 ├── types/ # TypeScript 类型定义 └── routes/ # 路由配置3.2 使用 Zustand 进行状态管理Zustand 是一个轻量、易用的状态管理库。我们为“任务”创建一个 Store。在src/stores/taskStore.ts中// src/stores/taskStore.ts import { create } from zustand; import { persist } from zustand/middleware; // 用于状态持久化 export interface Task { id: string; title: string; description?: string; status: todo | inProgress | done; // 任务状态 priority: low | medium | high; createdAt: Date; updatedAt: Date; } interface TaskStore { tasks: Task[]; draggedTaskId: string | null; // 当前被拖拽的任务ID // Actions addTask: (task: OmitTask, id | createdAt | updatedAt) void; updateTask: (id: string, updates: PartialTask) void; deleteTask: (id: string) void; setDraggedTaskId: (id: string | null) void; moveTask: (taskId: string, newStatus: Task[status]) void; } // 使用 persist 中间件将状态保存到 localStorage export const useTaskStore createTaskStore()( persist( (set) ({ tasks: [], draggedTaskId: null, addTask: (newTaskData) set((state) { const newTask: Task { ...newTaskData, id: task_${Date.now()}, createdAt: new Date(), updatedAt: new Date(), }; return { tasks: [...state.tasks, newTask] }; }), updateTask: (id, updates) set((state) ({ tasks: state.tasks.map((task) task.id id ? { ...task, ...updates, updatedAt: new Date() } : task ), })), deleteTask: (id) set((state) ({ tasks: state.tasks.filter((task) task.id ! id), })), setDraggedTaskId: (id) set({ draggedTaskId: id }), moveTask: (taskId, newStatus) set((state) ({ tasks: state.tasks.map((task) task.id taskId ? { ...task, status: newStatus, updatedAt: new Date() } : task ), })), }), { name: task-storage, // localStorage 中的 key } ) );这个 Store 定义了任务的数据结构、初始状态和所有修改状态的方法addTask, updateTask 等。persist中间件让我们的任务数据在页面刷新后也不会丢失。4. 完整实战构建任务看板现在我们开始实现核心功能——任务看板。4.1 路由配置与布局首先在src/App.tsx中配置应用路由和基础布局。// src/App.tsx import { BrowserRouter, Routes, Route, Navigate } from react-router-dom; import { Layout } from antd; import AppHeader from ./components/layout/AppHeader; import AppSider from ./components/layout/AppSider; import TaskBoardPage from ./features/taskBoard/TaskBoardPage; import DashboardPage from ./features/dashboard/DashboardPage; import SettingsPage from ./features/settings/SettingsPage; import ./App.css; const { Content } Layout; function App() { return ( BrowserRouter Layout style{{ minHeight: 100vh }} AppHeader / Layout AppSider / Content style{{ padding: 24px, background: #f0f2f5 }} Routes Route path/ element{Navigate to/board replace /} / Route path/board element{TaskBoardPage /} / Route path/dashboard element{DashboardPage /} / Route path/settings element{SettingsPage /} / Route path* element{div404 Page Not Found/div} / /Routes /Content /Layout /Layout /BrowserRouter ); } export default App;4.2 实现可拖拽的任务列与任务卡片这是看板的核心交互。我们使用dnd-kit来实现。首先创建任务列组件TaskColumn.tsx// src/features/taskBoard/components/TaskColumn.tsx import React from react; import { useDroppable } from dnd-kit/core; import { SortableContext, verticalListSortingStrategy } from dnd-kit/sortable; import { Badge, Card } from antd; import TaskCard from ./TaskCard; import { Task } from ../../../stores/taskStore; interface TaskColumnProps { id: string; title: string; tasks: Task[]; status: Task[status]; } const statusColor: RecordTask[status], string { todo: blue, inProgress: orange, done: green, }; const TaskColumn: React.FCTaskColumnProps ({ id, title, tasks, status }) { const { setNodeRef, isOver } useDroppable({ id, }); const taskIds tasks.map((task) task.id); return ( Card title{ div style{{ display: flex, justifyContent: space-between, alignItems: center }} span{title}/span Badge count{tasks.length} color{statusColor[status]} / /div } sizesmall style{{ height: 100%, border: isOver ? 2px dashed #1890ff : 1px solid #f0f0f0, backgroundColor: isOver ? #e6f7ff : white, }} div ref{setNodeRef} style{{ minHeight: 500px, transition: background-color 0.2s }} SortableContext items{taskIds} strategy{verticalListSortingStrategy} {tasks.map((task) ( TaskCard key{task.id} task{task} / ))} /SortableContext {tasks.length 0 ( div style{{ textAlign: center, color: #bfbfbf, padding: 20px }} 暂无任务可拖拽任务至此或创建新任务 /div )} /div /Card ); }; export default TaskColumn;接着创建可排序的任务卡片组件TaskCard.tsx// src/features/taskBoard/components/TaskCard.tsx import React from react; import { useSortable } from dnd-kit/sortable; import { CSS } from dnd-kit/utilities; import { Card, Tag, Dropdown, Button } from antd; import { MoreOutlined } from ant-design/icons; import type { Task } from ../../../stores/taskStore; import { useTaskStore } from ../../../stores/taskStore; interface TaskCardProps { task: Task; } const priorityColor: RecordTask[priority], string { high: red, medium: orange, low: green, }; const TaskCard: React.FCTaskCardProps ({ task }) { const { deleteTask, setDraggedTaskId } useTaskStore(); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } useSortable({ id: task.id }); const style { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, marginBottom: 8px, cursor: grab, }; const handleMenuClick (e: { key: string }) { if (e.key delete) { deleteTask(task.id); } }; const items [ { key: delete, label: span style{{ color: #ff4d4f }}删除任务/span, }, ]; return ( div ref{setNodeRef} style{style} {...attributes} {...listeners} onMouseDown{() setDraggedTaskId(task.id)} // 开始拖拽时记录任务ID onMouseUp{() setDraggedTaskId(null)} // 结束拖拽时清除 Card sizesmall title{ div style{{ display: flex, justifyContent: space-between }} span{task.title}/span Dropdown menu{{ items, onClick: handleMenuClick }} trigger{[click]} Button typetext icon{MoreOutlined /} sizesmall onClick{(e) e.stopPropagation()} / /Dropdown /div } p style{{ fontSize: 12px, color: #666 }}{task.description || 暂无描述}/p div style{{ display: flex, justifyContent: space-between, marginTop: 8px }} Tag color{priorityColor[task.priority]}{task.priority}/Tag span style{{ fontSize: 12px, color: #999 }} {new Date(task.updatedAt).toLocaleDateString()} /span /div /Card /div ); }; export default TaskCard;4.3 整合看板页面与拖拽上下文最后创建看板主页面TaskBoardPage.tsx它将三个任务列排列起来并包裹在DndContext中。// src/features/taskBoard/TaskBoardPage.tsx import React, { useState } from react; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, closestCorners } from dnd-kit/core; import { Row, Col, Button, Modal, Form, Input, Select } from antd; import { PlusOutlined } from ant-design/icons; import TaskColumn from ./components/TaskColumn; import TaskCard from ./components/TaskCard; import { useTaskStore, Task } from ../../stores/taskStore; const { Option } Select; const TaskBoardPage: React.FC () { const { tasks, draggedTaskId, moveTask } useTaskStore(); const [isModalVisible, setIsModalVisible] useState(false); const [form] Form.useForm(); // 根据状态筛选任务 const todoTasks tasks.filter((t) t.status todo); const inProgressTasks tasks.filter((t) t.status inProgress); const doneTasks tasks.filter((t) t.status done); const handleDragStart (event: DragStartEvent) { // 拖拽开始时可以设置一些全局状态已在 TaskCard 中处理 }; const handleDragEnd (event: DragEndEvent) { const { active, over } event; if (!over) return; const taskId active.id as string; const newStatus over.id as Task[status]; // over.id 对应 TaskColumn 的 id (即 status) // 如果拖拽到了不同的列则移动任务 const originalTask tasks.find((t) t.id taskId); if (originalTask originalTask.status ! newStatus) { moveTask(taskId, newStatus); } }; const handleAddTask (values: OmitTask, id | createdAt | updatedAt) { useTaskStore.getState().addTask(values); setIsModalVisible(false); form.resetFields(); }; const draggedTask tasks.find((t) t.id draggedTaskId); return ( div div style{{ marginBottom: 16px, display: flex, justifyContent: space-between }} h2智能任务看板/h2 Button typeprimary icon{PlusOutlined /} onClick{() setIsModalVisible(true)} 新建任务 /Button /div DndContext collisionDetection{closestCorners} onDragStart{handleDragStart} onDragEnd{handleDragEnd} Row gutter{[16, 16]} Col xs{24} sm{24} md{8} TaskColumn idtodo title待处理 tasks{todoTasks} statustodo / /Col Col xs{24} sm{24} md{8} TaskColumn idinProgress title进行中 tasks{inProgressTasks} statusinProgress / /Col Col xs{24} sm{24} md{8} TaskColumn iddone title已完成 tasks{doneTasks} statusdone / /Col /Row {/* 拖拽时的任务预览 */} DragOverlay {draggedTask ? TaskCard task{draggedTask} / : null} /DragOverlay /DndContext {/* 新建任务模态框 */} Modal title新建任务 open{isModalVisible} onOk{() form.submit()} onCancel{() { setIsModalVisible(false); form.resetFields(); }} okText创建 cancelText取消 Form form{form} layoutvertical onFinish{handleAddTask} Form.Item label任务标题 nametitle rules{[{ required: true, message: 请输入任务标题 }]} Input placeholder请输入任务标题 / /Form.Item Form.Item label任务描述 namedescription Input.TextArea rows{3} placeholder请输入任务描述可选 / /Form.Item Form.Item label优先级 namepriority initialValuemedium rules{[{ required: true }]} Select Option valuelow低/Option Option valuemedium中/Option Option valuehigh高/Option /Select /Form.Item Form.Item label初始状态 namestatus initialValuetodo rules{[{ required: true }]} Select Option valuetodo待处理/Option Option valueinProgress进行中/Option Option valuedone已完成/Option /Select /Form.Item /Form /Modal /div ); }; export default TaskBoardPage;至此一个具备拖拽功能的任务看板就完成了。运行pnpm dev启动开发服务器你可以在浏览器中看到效果并进行任务的创建、拖拽移动和删除。5. 数据可视化仪表盘实现看板页面完成后我们来实现数据统计页面。在src/features/dashboard/DashboardPage.tsx中集成 ECharts。// src/features/dashboard/DashboardPage.tsx import React from react; import { Row, Col, Card, Statistic } from antd; import { CheckCircleOutlined, ClockCircleOutlined, SyncOutlined } from ant-design/icons; import ReactECharts from echarts-for-react; import { useTaskStore } from ../../stores/taskStore; const DashboardPage: React.FC () { const { tasks } useTaskStore(); // 计算统计数据 const totalTasks tasks.length; const completedTasks tasks.filter((t) t.status done).length; const inProgressTasks tasks.filter((t) t.status inProgress).length; const completionRate totalTasks 0 ? ((completedTasks / totalTasks) * 100).toFixed(1) : 0.0; // 准备ECharts数据 - 按优先级统计 const priorityData [ { value: tasks.filter((t) t.priority high).length, name: 高 }, { value: tasks.filter((t) t.priority medium).length, name: 中 }, { value: tasks.filter((t) t.priority low).length, name: 低 }, ]; // 准备ECharts数据 - 每日完成趋势模拟 const trendData [5, 8, 12, 6, 15, 10, 7]; // 假设过去7天的完成数 const pieOption { tooltip: { trigger: item }, legend: { orient: vertical, left: left }, series: [ { name: 任务优先级分布, type: pie, radius: 50%, data: priorityData, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: rgba(0, 0, 0, 0.5), }, }, }, ], }; const lineOption { xAxis: { type: category, data: [周一, 周二, 周三, 周四, 周五, 周六, 周日], }, yAxis: { type: value }, series: [ { data: trendData, type: line, smooth: true, areaStyle: {}, }, ], tooltip: { trigger: axis }, }; return ( div h2数据仪表盘/h2 Row gutter{[16, 16]} style{{ marginBottom: 24px }} Col xs{24} sm{12} md{6} Card Statistic title总任务数 value{totalTasks} prefix{ClockCircleOutlined /} / /Card /Col Col xs{24} sm{12} md{6} Card Statistic title进行中 value{inProgressTasks} prefix{SyncOutlined spin /} / /Card /Col Col xs{24} sm{12} md{6} Card Statistic title已完成 value{completedTasks} prefix{CheckCircleOutlined /} / /Card /Col Col xs{24} sm{12} md{6} Card Statistic title完成率 value{completionRate} suffix% / /Card /Col /Row Row gutter{[16, 16]} Col xs{24} md{12} Card title任务优先级分布 ReactECharts option{pieOption} style{{ height: 400px }} / /Card /Col Col xs{24} md{12} Card title本周任务完成趋势 ReactECharts option{lineOption} style{{ height: 400px }} / /Card /Col /Row /div ); }; export default DashboardPage;6. 常见问题与排查思路在实现上述功能时你可能会遇到一些典型问题。以下是一些常见问题的排查思路问题现象可能原因解决思路拖拽功能无效卡片无法移动1.dnd-kit相关依赖未正确安装。2.DndContext未包裹拖拽区域。3.SortableContext的items属性未提供正确的唯一ID数组。1. 检查package.json并重新安装依赖。2. 确保可拖拽的组件如TaskCard和可放置区域如TaskColumn都在同一个DndContext下。3. 确认传递给SortableContext的items是当前列中所有任务id的数组。任务状态更新了但界面不刷新1. Zustand Store 的状态更新未触发组件重渲染。2. 组件未正确订阅 Store 中的状态。1. 确保在组件中使用useTaskStore()Hook 来获取状态和操作Zustand 会自动处理订阅。2. 如果是在 Store 外部手动调用 action确保是通过useTaskStore.getState().actionName()调用但这不会触发渲染应优先在组件内调用。ECharts 图表不显示或报错1.echarts和echarts-for-react版本不兼容。2. 图表容器的宽度或高度为0。3.option配置错误。1. 检查并安装兼容版本通常安装最新稳定版即可。2. 为包裹ReactECharts的容器设置明确的宽高如示例中的style{{ height: 400px }}。3. 使用 ECharts 官方配置项手册核对option格式。生产构建后页面空白或资源4041. Vite 的base配置不正确如果部署到子路径。2. 路由使用了 BrowserRouter 但服务器未配置 SPA 回退。1. 检查vite.config.ts中的base设置。2. 如果部署到 Nginx 等服务器需要配置try_files $uri $uri/ /index.html;将所有请求重定向到index.html。Ant Design 组件样式丢失1. 未正确引入 Ant Design 的样式文件。1. 在项目的入口文件如src/main.tsx顶部添加import antd/dist/reset.css;。Vite 项目通常使用此方式。7. 最佳实践与工程建议将功能跑通只是第一步要打造一个健壮、可维护的项目还需要遵循一些最佳实践。1. 状态管理规范化单一数据源确保任何一块数据只存在于一个 Store 中避免状态分散和同步难题。不可变更新在 Zustand 的 set 函数中始终返回一个新的状态对象或数组而不是直接修改原状态。本文示例中使用的map和filter都是返回新数组。按需订阅对于大型对象可以使用选择器selector来订阅部分状态避免不必要的重渲染。例如const tasks useTaskStore(state state.tasks)。2. 组件设计原则职责单一每个组件只做一件事。例如TaskCard只负责展示和拖拽单个任务不处理业务逻辑。Props 类型化使用 TypeScript 严格定义组件的 Props 接口提高代码可读性和安全性。提取自定义 Hook将可复用的逻辑如表单处理、数据获取提取到src/hooks/目录下的自定义 Hook 中。3. 性能优化点列表渲染优化如果任务列表变得非常长应考虑使用虚拟滚动库如react-window来优化性能。图表按需加载ECharts 体积较大可以考虑使用import()动态导入或只在仪表盘页面加载。构建优化利用 Vite 的代码分割能力并通过rollupOptions配置外部依赖如react,react-dom利用浏览器缓存。4. 生产环境部署环境变量使用.env.production和.env.development文件管理不同环境的 API 地址等配置。Vite 通过import.meta.env访问。错误边界使用React.ErrorBoundary包裹应用或路由组件捕获并优雅地处理运行时错误避免整个应用白屏。CI/CD 集成将代码检查pnpm lint、类型检查pnpm type-check和单元测试集成到 Git 钩子或 CI 流水线中确保代码质量。通过这个“智能任务看板”项目我们不仅实践了 React TypeScript Vite 的现代前端技术栈组合还深入应用了状态管理、拖拽交互、数据可视化等核心功能。从项目搭建到功能实现再到性能优化与部署考量我们走完了一个前端应用开发的完整生命周期。建议你在理解本文代码的基础上尝试扩展更多功能如任务标签、截止日期、用户协作、后端接口联调等将其打磨成一个更贴近实际业务的个人作品。