简介:这是一份基于 React 与 antd 的环境监测网站课程设计完整资源,面向正在完成前端项目实践或需要参考中后台交互方案的高校学生与初级开发者。系统采用 B/S 架构,前端以 React+antd 组件库为核心,融入 AJAX、HTML5、CSS3 等技术,实现环境数据上传、查看、项目分配与工作分类管理;内置 mock 数据,并能在页面不刷新情况下暂存数据,整体覆盖数据分析、数据监控、用户工作台、财务管理、项目管理及人员管理六大功能模块。压缩包共 98 个文件,以 jsx 组件源码、css 样式、png 界面截图为主,另有 json 配置、webpack 打包配置、说明文档及毕业论文 docx,包体仅 3.96MB,目录结构清晰,便于分模块阅读和二次开发。目前已有 176 人学习下载,适合需要系统掌握 React+antd 中后台开发流程、组件拆分思路与模拟数据交互的读者参考。
1. 环境监测网站不是一张大屏:拆开 React + antd 的六块业务边界
很多人拿到环境监测网站的第一反应是“放一堆图表在大屏上”,但真正动手用 React + antd 做过后台管理系统的人会告诉你,难点从来不在画图,而在业务状态怎么流转。这个项目让我印象最深的是它没有把精力耗在炫酷特效上,而是老老实实分了六个板块:数据分析、数据监控、用户工作台、财务管理、项目管理、人员管理。整体采用 B/S 架构,前端基于 React + antd,数据先用 mock JSON 驱动,页面不刷新也能保留操作结果。这种做法非常适合课程设计、毕设和中小团队内部系统起步:低门槛、组件齐、状态可控。下面我会按实际开发顺序把这套系统的关键设计拆开讲,从工程化搭建、数据接入、状态管理,到最后 antd Table 的动态列合并技巧,每个环节都给出可直接抄的配置和代码。
2. 从零搭出可维护的 React + antd 工程:webpack、babel 与目录边界
2.1 为什么不用 Create React App 而要手写 webpack 配置
现在的脚手架默认隐藏 webpack,但这个项目里保留了完整的webpack.config.js和webpack.production.config.js。我在接管这类代码时并不觉得多余,反而认为这是最值得讲清楚的部分。手写 webpack 的价值在于你能精确控制入口、别名和 mock 转发,这些在环境监测这种需要对接大量 JSON 数据的场景里非常实用。
先看开发环境的入口配置,我简化掉不必要的插件后是这样:
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/main.jsx', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.[contenthash:8].js' }, resolve: { extensions: ['.js', '.jsx', '.json'] }, module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] } } }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] }, devServer: { port: 3000, hot: true, historyApiFallback: true } };这段配置里有三个参数值得说明。entry指向main.jsx,这是所有组件的起点,后面接入 Redux 时也要从这里包一层Provider。extensions让 JSX 文件可以不带后缀引入,省去一堆import x from './x.jsx'。historyApiFallback必须打开,因为 React Router 用的可能是 BrowserRouter,路由路径在刷新时会 404。
2.2 目录结构决定了状态流的边界
原项目里除了 webpack 配置,还会看到app、containers、components、routes、stores、actions、constants、reducers、static这样一套目录。这套结构在 2024 年的 React 社区里仍然不过时,因为它把“页面容器”“纯组件”“全局状态”三个层次切得很干净。
我一般会建议按这个边界来组织:
src/ ├── components/ # 无状态组件:按钮区、表格列、图表卡片 ├── containers/ # 有业务逻辑的页面容器,连接 store ├── routes/ # 路由表,单独拆出来方便做权限控制 ├── stores/ # 创建单一 store ├── actions/ # action creator ├── reducers/ # 状态更新逻辑 ├── constants/ # action type 常量 ├── static/ # 静态资源、mock 图片 └── main.jsx # 入口文件很多新人在做环境监测这类系统时会把“数据获取”直接写进components,结果按钮组件里出现axios调用,这个模块马上就不干净了。正确方式是 components 只负责显示和抛出事件,数据请求放在containers或单独的服务层。原项目里的图片编号从 1 到 25,基本对应各个页面的截图,当时应该是为了写论文用的,说明作者对界面完整度很在意。
2.3 antd 组件选型以“表格 + 表单 + 布局”为核心
环境监测后台的重头戏不是组件库有多全,而是能不能快速拼出信息密度高的管理界面。antd 在这类 B 端场景里优势非常明显:Table、Form、Layout、Menu是四个高频件,几乎每个板块都用得上。
比如项目管理的页面一定会长这样:
import { Table, Tag, Button, Space } from 'antd'; const columns = [ { title: '项目编号', dataIndex: 'id', width: 100 }, { title: '项目名称', dataIndex: 'name' }, { title: '状态', dataIndex: 'status', render: (status) => ( <Tag color={status === '进行中' ? 'green' : 'red'}>{status}</Tag> ) }, { title: '操作', render: (_, record) => ( <Space> <Button size="small" onClick={() => handleAllocate(record)}>分配</Button> <Button size="small" danger onClick={() => handleDelete(record)}>删除</Button> </Space> ) } ];Space用来做按钮间距,Tag用来区分环境监测项目的执行状态,操作列的 render 函数能直接拿到当前行数据。这个例子说明 antd 表格不需要引入额外封装的业务组件,就能覆盖大部分“查看环境数据 + 分配项目”的需求。
3. 数据监控与趋势图:Mock JSON、页面级存储与图表渲染
3.1 Mock 数据的两种常见做法
原项目明确说“数据将使用自己定义的 mock 数据”,并且要求“在页面不重新启动的情况下有保存数据的功能”。这句话翻译过来就是:需要一个本地状态的 mock server,或者至少是一个模块级内存 store。我见过很多人用 json-server 起一个全局 mock,但环境监测数据一旦重新刷新就全丢了,与需求不符。
更稳妥的做法是在前端实现一个模拟接口层。例如新增环境监测数据时,数据先写入模块内的数组,再模拟网络延迟返回:
// mock/api.js let monitorList = []; export const fetchMonitorData = () => { return new Promise((resolve) => { setTimeout(() => resolve({ code: 200, data: monitorList }), 200); }); }; export const uploadMonitorData = (payload) => { return new Promise((resolve) => { monitorList = [payload, ...monitorList]; setTimeout(() => resolve({ code: 200, message: '上传成功' }), 200); }); };使用这样的 mock 层时,页面组件不需要感知数据是不是真的,统一走async/await调用。这样做的好处是将来接真实接口时,只需替换mock/api.js内部实现,所有调用方都不用动。
3.2 用 antd Table + ECharts 展示实时监控数据
数据监控页的核心是两张图:趋势折线图和实时数据表格。ECharts 在 React 里最常用的封装方式不是引入重型组件,而是直接用useEffect初始化实例,这样可控性最高。
import { useEffect, useRef } from 'react'; import * as echarts from 'echarts'; import { Card } from 'antd'; export default function MonitorChart({ data }) { const chartRef = useRef(null); useEffect(() => { const chart = echarts.init(chartRef.current); chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.map(item => item.time) }, yAxis: { type: 'value', name: 'PM2.5' }, series: [{ name: '空气质量指数', type: 'line', smooth: true, data: data.map(item => item.value) }] }); return () => chart.dispose(); }, [data]); return <Card title="实时监测趋势"><div ref={chartRef} style={{ height: 320 }} /></Card>; }这里的useEffect依赖data,只要上传接口返回新数据,图表就会自动刷新。chart.dispose()在组件卸载时销毁实例,避免内存泄漏。数据监控页除了图表还会配一个 antd 的Table,和图表共用同一个data源,但从不同的维度展示。表格列里通常会有数值单位和超标状态两列,状态列用Tag颜色区分,比如TAG_COLOR = { 正常: 'green', 预警: 'orange', 超标: 'red' }。
3.3 页面不重启保存数据的状态管理细节
需求里提到的“保存数据”,本质上是把数据从组件内部的useState提升到更外层。最简单的方式是放到reducer里,因为 Redux store 在页面整个生命周期内都常驻。有人会用sessionStorage,但它需要序列化,而且只能存字符串,在复杂对象上容易出 bug。
我建议遵守原项目的设计,所有新增数据通过 action 提交到 reducer,例如:
// actions/monitor.js import { ADD_MONITOR_DATA } from '../constants/actionTypes'; export const addMonitorData = payload => ({ type: ADD_MONITOR_DATA, payload });然后在 reducer 中这样写入:
const initialState = { monitorRecords: [] }; export default function monitorReducer(state = initialState, action) { switch (action.type) { case ADD_MONITOR_DATA: return { ...state, monitorRecords: [action.payload, ...state.monitorRecords] }; default: return state; } }这个动作完成后,即使切换到其他页面再切回来,数据依然在。唯一需要注意的是 Redux 默认不支持持久化,浏览器刷新后还是会丢,但原需求只要求“页面不重新启动”,所以完全达标。如果将来要刷新不丢,得再接入redux-persist或者本地缓存,那是另一个话题。
4. 用户工作台与权限路由:Redux 数据流和 Roles 的落地做法
4.1 工作台为什么需要 Redux 而不是 Context
这个系统里有用户工作台、财务管理和人员管理,多个页面之间会共享当前用户、待办任务和项目分配状态。如果只用useContext,状态一变所有消费组件都会重渲染,性能不好控制。Redux 的connect或useSelector则能精确订阅需要的片段。原项目把actions、constants、reducers、stores分开,正是在为标准 Redux 数据流做准备。
数据结构可以这样定义:
// reducers/user.js const initialState = { currentUser: null, todoList: [], role: '' }; // constants/actionTypes.js export const SET_CURRENT_USER = 'SET_CURRENT_USER'; export const UPDATE_TODO_LIST = 'UPDATE_TODO_LIST';工作台页面加载时,会先派发fetchCurrentUser这个 action,然后通过useSelector读取用户信息和控制台统计卡片。典型代码是:
import { useSelector, useDispatch } from 'react-redux'; import { fetchCurrentUser } from '../actions/user'; import { Card, List, Avatar } from 'antd'; import { useEffect } from 'react'; const Workbench = () => { const dispatch = useDispatch(); const { currentUser, todoList } = useSelector(state => state.user); useEffect(() => { dispatch(fetchCurrentUser()); }, [dispatch]); return ( <Card title={`${currentUser?.name || '未知用户'} 的工作台`}> <List dataSource={todoList} renderItem={(item) => ( <List.Item> <List.Item.Meta avatar={<Avatar>{item.type[0]}</Avatar>} title={item.title} description={item.priority === 'high' ? '高优先级' : '普通'} /> </List.Item> )} /> </Card> ); };这里useSelector的参数是回调函数,返回state.user对象。需要注意,如果返回的是新的对象引用,哪怕内容没变也可能触发渲染。保险起见分开取currentUser和todoList两个useSelector调用更稳妥。
4.2 路由表怎么配合人员管理与财务模块
人员管理和财务管理应该是不同角色看到的,所以路由不能静态写死。常见做法是配置私有路由组件,用role字段做拦截:
import { Navigate } from 'react-router-dom'; const PrivateRoute = ({ role, children }) => { const userRole = useSelector(state => state.user.role); if (!userRole) return <Navigate to="/login" replace />; if (role && userRole !== role) return <Navigate to="/403" replace />; return children; };把这个组件套在路由上:
<Route path="/finance" element={ <PrivateRoute role="admin"> <FinancePage /> </PrivateRoute> } />在环境监测这个场景里,普通用户应该能看数据监控和数据分析,只有管理员能进财务和人员管理。这个role也可以是数组,比如['admin', 'manager'],需要做数组包含判断。路由守卫不是安全边界,它只是控制 UI 入口,真正安全依然要在后端做鉴权。
4.3 工作分类与项目分配的状态联动
项目分配是整个系统里最容易写乱的部分。分配项目时会涉及三个模块:项目列表、用户工作台、人员管理。比如管理员在“项目管理”里把项目分配给某人,这个人的“工作台待办”应立即多一条记录。用 Redux 可以这样设计:
const onAllocate = (projectId, userId) => { dispatch(allocateProject(projectId, userId)); dispatch(updateTodoList(projectId, userId)); };但更合理的做法是封装一个业务 service,内部连续派发两个 action,组件只调用一个函数。在 reducer 里要注意两个状态更新的边界:项目分配是把project.userId改成目标用户,待办列表是往user.todoList里追加一条数据。两边不共享字段,所以不推荐用一个 reducer 强行处理所有模块,按领域拆 reducer 才便于维护。
5. antd Table 动态合并列与监控告警表格的优化技巧
环境监测的数据监控页常常需要把同一监测点、同一时间的多行数据列合并展示,样式上更接近“分组报告”。antd 的Table内置onCell方法支持动态合并,做法是给每一个单元格返回rowSpan,该值为 0 时隐藏,为大于 1 的整数时向下合并。
const mergeCells = (data, key) => { const result = {}; data.forEach((row, index) => { const value = row[key]; if (result[value]) { result[value].end = index; } else { result[value] = { start: index, end: index }; } }); return result; }; const columns = [ { title: '监测点', dataIndex: 'station', onCell: (record, index) => { const range = mergeCells(data, 'station')[record.station]; if (!range) return {}; if (index === range.start) { return { rowSpan: range.end - range.start + 1 }; } return { rowSpan: 0 }; } }, { title: '时间', dataIndex: 'time' }, { title: '指标', dataIndex: 'metric' }, { title: '数值', dataIndex: 'value' } ];这段代码里mergeCells先把同一监测点出现的起止行算出来,然后只在起始行返回rowSpan,其余行返回 0,antd 会自动隐藏格子并完成纵向合并。需要注意,onCell的第二个参数是行索引,当表格分页时它指的是当前页内的索引,所以合并计算必须基于当前页的data,不能基于全部数据。数据量大的话,建议把mergeCells的结果缓存成 Map,不要每行每次都重新遍历,否则 1000 行内就能感受到卡顿。
最后补一个常用的性能优化点:当监控数据每 5 秒刷新一次时,表格会频繁重渲染。我给Table的rowKey设成record.id,同时给每列加shouldCellUpdate控制,只有数值变化时才更新单元格。再配合pagesize控制在 20 行以内,antd Table 在这种轻量后台场景下完全扛得住。动态合并列和性能优化是两个独立的技巧,建议先写出不合并的版本,确认数据正确后再加rowSpan,避免排查问题时分不清是数据错还是合并逻辑错。
本文还有配套的精品资源,点击获取