2023-03-1 MERN全栈项目——记账本

2023-03-01 MERN

一、配置前端

1、初始化ReactAPP

  1. 创建文件夹
  • 在桌面创建expense tracker 文件夹
  1. 命令
  1. npx create-react-app client 
  2. cd client 
  3. npm start 
  1. 清理文件夹,进入src文件夹
  • 进入App.js,删除return中的header内容及引入的logo,补充文本内容:我的记账本
  1. import './App.css' 
  2.  
  3. function App() { 
  4. return <div className="App"> 
  5. <h1>我的记账本</h1> 
  6. </div>; 
  7. } 
  8.  
  9. export default App; 
  • 删除logo.svg
  • 清空App.css样式

2、配置文件结构及安装模块

  1. 安装三方模块
  1. npm install antd react-router-dom aos react-redux redux axios 
  2. //Axios,是一个基于promise的网络请求库,作用于node.js和浏览器中 
  3. //AOS(Animate on scroll)是小型动画滚动库,可在页面滚动时给元素添加动画效果。 
  1. 重启
  1. npm start 
  1. antd官网:https://ant.design/index-cn and of react文档:https://ant.design/docs/react/introduce-cn
  1. antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。 
  • 进入组件,找到Button按钮,并拷贝primary button的代码插入到App.js进行展示
  1. import { Button} from 'antd'; 
  2. <Button type="primary">Primary Button</Button> 
  1. 配置文件目录
  • 创建src/pages文件夹 用于存放项目页面组件
  • 创建src/components 用于存放项目中公用的组件
  • 创建src/resources 用于存放公共静态资源
  • 创建src/redux 用于存放状态管理
  1. 引入bootstrap:https://getbootstrap.com/, 拷贝CDN链接,进入public/index.html进行粘贴,清楚注释掉的内容,修改title。
  1. <!-- CSS only --> 
  2. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> 

3、创建默认布局组件

  1. 创建页面组件文件及默认布局组件
  • 创建src/pages/Home.js
  • 创建src/components/DefaultLayout.js
  1. 进入DefaultLayout.js:
  1. import React from "react"; 
  2.  
  3. function DefaultLayout(props) { 
  4.  
  5. return ( 
  6. <div className="layout"> 
  7. <div className="header"> 
  8. <div> 
  9. <h1 className="logo">我的记账本</h1> 
  10. </div> 
  11. </div> 
  12.  
  13. <div className="content">{props.children}</div> 
  14. </div> 
  15. ); 
  16. } 
  17.  
  18. export default DefaultLayout; 
  19.  
  1. 进入Home.js
  1. import React from "react"; 
  2. import DefaultLayout from '../components/DefaultLayout' 
  3.  
  4. function Home() { 
  5. return ( 
  6. <DefaultLayout> 
  7. <h1>这是主页</h1> 
  8. </DefaultLayout> 
  9. ); 
  10. } 
  11.  
  12. export default DefaultLayout; 
  1. 进入App.js
  1. import "./App.css"; 
  2. import { BrowserRouter as Routers, Route, Routes } from "react-router-dom"; 
  3. import Home from "./pages/Home"; 
  4.  
  5. function App() { 
  6. return ( 
  7. <div className="App"> 
  8. <Routers> 
  9. <Routes> 
  10. <Route path="/" element={<Home />} /> 
  11. </Routes> 
  12. </Routers> 
  13. </div> 
  14. ); 
  15. } 
  16.  
  17. export default App; 
  18.  
  1. 创建src/pages/Test.js文件,测试路由
  1. import React from 'react' 
  2. import DefaultLayout from '../components/DefaultLayout' 
  3.  
  4. function Test() { 
  5. return ( 
  6. <DefaultLayout><h1>这是测试页面</h1></DefaultLayout> 
  7. ) 
  8. } 
  9.  
  10. export default Test 
  1. Test.js文件引入App.js中:
  1. import "./App.css"; 
  2. import { BrowserRouter as Routers, Route, Routes } from "react-router-dom"; 
  3. import Home from "./pages/Home"; 
  4. import Test from "./pages/Test"; 
  5.  
  6. function App() { 
  7. return ( 
  8. <div className="App"> 
  9. <Routers> 
  10. <Routes> 
  11. <Route path="/" element={<Home />} /> 
  12. <Route path="/test" element={<Test />} /> 
  13. </Routes> 
  14. </Routers> 
  15. </div> 
  16. ); 
  17. } 
  18.  
  19. export default App; 

4、添加默认布局组件样式

  1. 首先进入index.js,引入Googlefonts字体图标:ZCOOL XiaoWei
  1. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap'); 
  2. body { 
  3. margin: 0; 
  4. font-family: 'Noto Sans SC', sans-serif; 
  5. } 
  6.  
  1. 创建src/resources/default-layout.css
  1. .layout{ 
  2. margin: 0 100px; 
  3. } 
  4. .header{ 
  5. background-color: #1677FF 
  6. padding: 20px; 
  7. border-bottom-right-radius: 25px; 
  8. border-bottom-left-radius: 25px; 
  9. } 
  10. .logo{ 
  11. font-size: 30px; 
  12. color: rgba(255, 255, 255, 0.716); 
  13. margin: 0 !important; 
  14. cursor: pointer; 
  15. } 
  16.  
  17. .username{ 
  18. font-size: 18px; 
  19. color: rgba(255, 255, 255, 0.742); 
  20. } 
  21.  
  22. .content{ 
  23. height: 85vh; 
  24. box-shadow: 0 0 2px gray; 
  25. margin-top: 20px; 
  26. border-top-right-radius: 25px; 
  27. border-top-left-radius: 25px; 
  28. padding: 15px; 
  29. } 
  30.  
  31. @media screen and (max-width:700px){ 
  32. .layout{ 
  33. margin: 0 15px; 
  34. } 
  35. } 
  1. 将default-layout.css引入DefaultLayout.js
  1. import React from "react"; 
  2. import "../resources/default-layout.css"; 
  3. function DefaultLayout(props) { 
  4.  
  5. return ( 
  6. <div className="layout"> 
  7. <div className="header d-flex justify-content-between align-items-center"> 
  8. <div> 
  9. <h1 className="logo">我的记账本</h1> 
  10. </div> 
  11. <div> 
  12. <h1 className="username">用户姓名</h1> 
  13. </div> 
  14. </div> 
  15.  
  16. <div className="content">{props.children}</div> 
  17. </div> 
  18. ); 
  19. } 
  20.  
  21. export default DefaultLayout; 
  22.  
  1. 测试样式,若引入的外部字体无效,可以进入index.css,设置为最高级
  1. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap'); 
  2. body { 
  3. margin: 0; 
  4.  
  5. font-family: 'Noto Sans SC', sans-serif !important; 
  6. } 
  7.  
  8.  

5.测试

  1. npm start 

二、配置后台

1、初始化server服务器

  1. 进入expense tracker文件夹,初始化项目
  1. npm init 
  1. 安装第三方软件包
  1. npm install nodemon mongoose express 
  1. 创建expense tracker/server.js文件:
  1. const express = require('express') 
  2. const app = express() 
  3. const PORT =process.env.PORT || 5000 
  4.  
  5. app.get('/',(req,res) => res.send('Hello World!')) 
  6. app.listen(PORT, () => console.log(`服务器正在${PORT}端口号运行...`);) 
  1. 监听server服务器
  1. nodemon server 

2、连接MongoDB数据库

  1. 登录MongoDB Atlas: https://www.mongodb.com/atlas/database
  2. 执行流程:
  1. 第一步创建新的database数据库:点击Atlas面板中的Browse Collections 
  2. 第二步点击Create Database 按钮,输入数据库名称及集群名称 
  3. 第三步点击Overview返回主页面板,并点击connect连接按钮 
  4. 第四步点击connect using MongoDB Compass 
  5. 第五步如果没有MongoDB Compass面板,就点击进行安装,如果有就拷贝连接到mongoDB Compass 的连接字符串 
  6. 第六步打开MongoDB Compass面板粘贴字符串进行连接: 
  7. mongodb+srv://<username>:<password>@msonline.menjs.mongodb.net/expense-tracker 
  8. 第七步实现nodejs连接到mongoDB数据库 
  1. 创建expense-tracker/dbConnect.js:
  1. const mongoose = require('mongoose'); 
  2.  
  3. mongoose.connect( 
  4. 'mongodb+srv://msonline123:test123456@msonline.menjs.mongodb.net/expense-tracker' 
  5. { useNewUrlParser: true, useUnifiedTopology: true } 
  6. ); 
  7.  
  8. const connection = mongoose.connection; 
  9.  
  10. connection.on('error', (err) => console.log(err)); 
  11.  
  12. connection.on('connected', () => 
  13. console.log('MongoDB数据库连接成功!') 
  14. ); 
  15.  
  1. 进入server.js
  1. const dbConnect = require('./dbConnect') 

三、用户登录/注册UI

1、注册页面组件

  1. 推荐颜色取值器【ColorPick Eyedroppe】:https://chrome.google.com/webstore/detail/colorpick-eyedropper/ohcpnigalekghcmgcdcenkpelffpdolg
  2. 分析登录/注册页面结构
  3. 使用antd提供的表单组件来进行构建:https://ant.design/components/form-cn, 优势不需要使用reacthooks或者state状态来声明。
  4. 配置页面组件文件:
  • 创建src/pages/Login.js 用于放置登录页面组件内容
  • 创建src/pages/Register.js 用于放置注册页面组件内容
  1. 进入Login.js :
  1. import React from 'react' 
  2.  
  3.  
  4. function Login() { 
  5. return ( 
  6. <div>这是登录页面</div> 
  7. ) 
  8. } 
  9.  
  10. export default Login 
  1. 进入Register.js:
  1. import React from 'react' 
  2.  
  3.  
  4. function Register() { 
  5. return ( 
  6. <div>这是注册页面</div> 
  7. ) 
  8. } 
  9.  
  10. export default Register 
  1. 进入App.js,引入上方两个新的页面组件,并测试
  1. import "./App.css"; 
  2. import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; 
  3. import Home from "./pages/Home"; 
  4. import Test from "./pages/Test"; 
  5. import Login from "./pages/Login"; 
  6. import Register from "./pages/Register"; 
  7.  
  8. function App() { 
  9. return ( 
  10. <div className="App"> 
  11. <BrowserRouter> 
  12. <Routes> 
  13. <Route path="/" element={<Home />} /> 
  14. <Route path="/test" element={<Test />} /> 
  15. <Route path='/login' element={<Login />} /> 
  16. <Route path='/register' element={<Register />} /> 
  17. </Routes> 
  18. </BrowserRouter> 
  19. </div> 
  20. ); 
  21. } 
  22.  
  23. export default App; 
  24.  
  1. 进入Register.js,添加样式结构:
  1.  
  2. import React from "react"; 
  3. import { Form, message } from "antd"; 
  4. import Input from "antd/lib/input/Input"; 
  5.  
  6. function Register() { 
  7.  
  8. return ( 
  9. <div className="register"> 
  10. <div className="row "> 
  11. <div className="col-md-5"> 
  12. {/*左侧图片*/} 
  13. </div> 
  14. <div className="col-md-4"> 
  15. {/*右侧表单*/} 
  16. {/*vertical的作用是进行垂直对齐*/} 
  17. <Form layout="vertical" > 
  18. <h1>用户注册</h1> 
  19.  
  20. <Form.Item label="姓名" name="name"> 
  21. <Input /> 
  22. </Form.Item> 
  23. <Form.Item label="邮箱" name="email"> 
  24. <Input /> 
  25. </Form.Item> 
  26. <Form.Item label="密码" name="password"> 
  27. <Input type="password" /> 
  28. </Form.Item> 
  29.  
  30. <div className="d-flex justify-content-between align-items-center"> 
  31. <Link to="/login">已经注册 ,点击进入登录页面</Link> 
  32. <button className="primary" type="submit"> 
  33. 注册 
  34. </button> 
  35. </div> 
  36. </Form> 
  37. </div> 
  38. </div> 
  39. </div> 
  40. ); 
  41. } 
  42.  
  43. export default Register; 
  44.  
  1. 进入index.css,添加全局的主题颜色:
  1. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap'); 
  2. body { 
  3. margin: 0; 
  4. font-family: 'Noto Sans SC', sans-serif; 
  5. } 
  6.  
  7. .primary{ 
  8. background-color: #1677FF; 
  9. padding:5px 20px; 
  10. color: white; 
  11. border: none; 
  12. border-radius: 3px; 
  13. } 

2、登录页面组件

  1. 一个免费下载各类动画素材的网站:https://lottiefiles.com/
  • 第一步先进行登录,若无账号可以进行注册
  • 搜索框中输入money,查询相关动画素材
  • 选择“Guilherme Lopes”,点击【html】按钮
  • 拷贝提供的scripe标签,粘贴到public中的index
  • 拷贝提供的粘贴到Register组件对应的位置,删除
  • 【style】【controls】属性
  1.  
  2. import React from "react"; 
  3. import { Form, message } from "antd"; 
  4. import Input from "antd/lib/input/Input"; 
  5.  
  6. function Register() { 
  7.  
  8. return ( 
  9. <div className="register"> 
  10. <div className="row "> 
  11. <div className="col-md-5"> 
  12. {/*左侧图片*/} 
  13. <lottie-player 
  14. src="https://assets3.lottiefiles.com/packages/lf20_06a6pf9i.json" 
  15. background="transparent" 
  16. speed="1" 
  17. loop 
  18. autoplay 
  19. ></lottie-player> 
  20. </div> 
  21. ... 
  22. </Form> 
  23. </div> 
  24. </div> 
  25. </div> 
  26. ); 
  27. } 
  28.  
  29. export default Register; 
  1. 设置Register中row的弹性布局:
  1. import React from "react"; 
  2. import { Form, message } from "antd"; 
  3. import Input from "antd/lib/input/Input"; 
  4.  
  5. function Register() { 
  6.  
  7. return ( 
  8. <div className="register"> 
  9. <div className="row justify-content-center align-items-center w-100 h-100"> 
  10. ... 
  11. </div> 
  12. </div> 
  13. ); 
  14. } 
  15.  
  16. export default Register; 
  1. 若出现滚动条,可以进入index.css:
  1. body,html{ 
  2. overflow-x: hidden; 
  3. } 
  4.  
  1. 添加Register中动画素材的样式,可以单独创建一个css外部样式:
  • 创建src/resources/authentication.css文件:
  1. .register{ 
  2. height:100vh; 
  3. display: flex; 
  4. align-items: center; 
  5. justify-content: center; 
  6. {/*进入webgradients.com,拷贝喜欢的渐变色-022*/} 
  7. background-image: linear-gradient(to right, #30cfd0 0%, #330867 100%); 
  8. } 
  9. .lottie{ 
  10. height:400px; 
  11. } 
  12.  
  13. .register input{ 
  14. background-color: transparent; 
  15. border: none; 
  16. border-bottom: 1px solid rgba(255, 255, 255, 0.784); 
  17. color: rgba(255, 255, 255, 0.568); 
  18. } 
  19.  
  20. input:focus{ 
  21. outline: none !important; 
  22. box-shadow: none !important; 
  23. border-bottom: 1px solid rgba(255, 255, 255, 0.784) !important; 
  24. } 
  25.  
  26. label , a{ 
  27. color: rgba(255, 255, 255, 0.536) !important; 
  28. } 
  29.  
  30. .register h1{ 
  31. font-size: 35px; 
  32. color: white; 
  33. font-weight: 600; 
  34. } 
  1. 表单提交设置:
  1. import { Form,Input} from "antd"; 
  2. import React from "react"; 
  3. import { Link } from "react-router-dom"; 
  4. import "../resources/authentication.css"; 
  5.  
  6.  
  7. function Register() { 
  8. //2 输出表单内容 
  9. const onFinish = async (values) => { 
  10. console.log(values) 
  11. }; 
  12.  
  13.  
  14. return ( 
  15. <div className="register"> 
  16. {loading && <Spinner />} 
  17. <div className="row justify-content-center align-items-center w-100 h-100"> 
  18. <div className="col-md-5"> 
  19. <div className="lottie"> 
  20. <lottie-player 
  21. src="https://assets3.lottiefiles.com/packages/lf20_06a6pf9i.json" 
  22. background="transparent" 
  23. speed="1" 
  24. loop 
  25. autoplay 
  26. ></lottie-player> 
  27. </div> 
  28. </div> 
  29. <div className="col-md-4"> 
  30. {/*1 添加onFinish:提交表单且数据验证成功后回调事件*/} 
  31. <Form layout="vertical" onFinish={onFinish}> 
  32. <h1>用户注册</h1> 
  33.  
  34. <Form.Item label="姓名" name="name"> 
  35. <Input /> 
  36. </Form.Item> 
  37. <Form.Item label="邮箱" name="email"> 
  38. <Input /> 
  39. </Form.Item> 
  40. <Form.Item label="密码" name="password"> 
  41. <Input type="password" /> 
  42. </Form.Item> 
  43.  
  44. <div className="d-flex justify-content-between align-items-center"> 
  45. <Link to="/login">已经注册 ,点击进入登录页面</Link> 
  46. <button className="primary" type="submit"> 
  47. 注册 
  48. </button> 
  49. </div> 
  50. </Form> 
  51. </div> 
  52. </div> 
  53. </div> 
  54. ); 
  55. } 
  56.  
  57. export default Register; 
  58.  
  1. 拷贝代码粘贴到Login.js中,并做修改:
  1. import { Form,Input} from "antd"; 
  2. import React from "react"; 
  3. import { Link } from "react-router-dom"; 
  4. import "../resources/authentication.css"; 
  5.  
  6.  
  7. function Login() { 
  8. const onFinish = async (values) => { 
  9. console.log(values) 
  10. }; 
  11.  
  12.  
  13. return ( 
  14. <div className="register"> 
  15. {loading && <Spinner />} 
  16. <div className="row justify-content-center align-items-center w-100 h-100"> 
  17. <div className="col-md-4"> 
  18. <Form layout="vertical" onFinish={onFinish}> 
  19. <h1>用户登录</h1> 
  20. <Form.Item label="邮箱" name="email"> 
  21. <Input /> 
  22. </Form.Item> 
  23. <Form.Item label="密码" name="password"> 
  24. {/*在不想使用缓存的input标签中添加 autocomplete="off"属性;*/} 
  25. <Input type="password" autocomplete=“new-password” /> 
  26. </Form.Item> 
  27.  
  28. <div className="d-flex justify-content-between align-items-center"> 
  29. <Link to="/register">还没有注册 ,点击进入注册页面</Link> 
  30. <button className="primary" type="submit"> 
  31. 登录 
  32. </button> 
  33. </div> 
  34. </Form> 
  35. </div> 
  36.  
  37. <div className="col-md-5"> 
  38. <div className="lottie"> 
  39. <lottie-player 
  40. src="https://assets3.lottiefiles.com/packages/lf20_06a6pf9i.json" 
  41. background="transparent" 
  42. speed="1" 
  43. loop 
  44. autoplay 
  45. ></lottie-player> 
  46. </div> 
  47. </div> 
  48. </div> 
  49. </div> 
  50. ); 
  51. } 
  52.  
  53. export default Login; 

四、用户登录/注册API

1、用户模型及API

  1. 首先创建数据库的models模型:
  • 创建expense-tacker/models/User.js
  1. const mongoose = require('mongoose') 
  2.  
  3. const userSchema = new mongoose.Schema({ 
  4. name : { 
  5. type : String, 
  6. required : true 
  7. }, 
  8. email:{ 
  9. type : String, 
  10. required : true 
  11. }, 
  12. password : { 
  13. type : String, 
  14. required : true 
  15. } 
  16. }) 
  17. //Model对象代表的是数据库中的(collection),通过Model才能对数据库进行操作 
  18. //第一个参数是modelName,代表的是你要和数据库中映射的集合名(默认是复数形式),第二个参数schema代表的是你刚刚创建的schema对象名。 
  19. const usermodel = mongoose.model('Users' , userSchema) 
  20.  
  21. module.exports = usermodel 
  1. 创建API:
  • 创建expense-tracker/routes/usersRoute.js:
  1. const express = require("express"); 
  2. const User = require("../models/User"); 
  3. //使用 express.Router 类创建模块化、可挂载的路由句柄 
  4. const router = express.Router(); 
  5.  
  6. router.post("/login", async function (req, res) { 
  7. try { 
  8. const result = await User.findOne({ 
  9. email: req.body.email, 
  10. password: req.body.password, 
  11. }); 
  12.  
  13. if (result) { 
  14. res.send(result); 
  15. } else { 
  16. res.status(500).json("Error"); 
  17. } 
  18. } catch (error) { 
  19. res.status(500).json(error); 
  20. } 
  21. }); 
  22.  
  23. router.post("/register", async function (req, res) { 
  24. try { 
  25. //创建用户 
  26. const newuser = new User(req.body); 
  27. await newuser.save(); 
  28. res.send('新用户注册成功!') 
  29. } catch (error) { 
  30. res.status(500).json(error); 
  31. } 
  32. }); 
  33.  
  34. //暴露 router模块 
  35. module.exports = router; 
  1. 进入server.js,将路由添加到应用当中:
  1. const express = require('express') 
  2. const dbConnect = require('./dbConnect') 
  3. const userRoute = require('./routes/userRoute') 
  4. const app = express() 
  5. const PORT =process.env.PORT || 5000 
  6.  
  7. //express.json()是Express 中内置的中间件功能。此方法用于解析带有 JSON 有效负载的传入请求,并基于 bodyparser。 
  8. app.use(express.json()) 
  9.  
  10. //配置路由 
  11. app.use('/api/users/',userRoute) 
  12.  
  13.  
  14.  
  15. app.listen(PORT, () => console.log(`服务器正在${PORT}端口号运行...`);) 

2、测试登录和注册

  1. 进入client/package.json,配置proxy:
  1. //前端配置跨域代理  
  2. "proxy": "http://localhost:5000" 
  1. 进入Register.js,使用axios发起请求:
  1. import { Form,Input,message} from "antd"; 
  2. import React from "react"; 
  3. import { Link } from "react-router-dom"; 
  4. import "../resources/authentication.css"; 
  5. //1 引入模块 
  6. import axios from 'axios' 
  7.  
  8. function Register() { 
  9.  
  10. const onFinish = async (values) => { 
  11. //2 发起请求 
  12. try { 
  13. await axios.post("/api/users/register", values); 
  14. message.success("注册成功!"); 
  15. } catch (error) { 
  16. message.error("抱歉,出错了!"); 
  17. } 
  18. }; 
  19.  
  20. return ( 
  21. ... 
  22.  
  23. } 
  24.  
  25. export default Register; 
  1. 进入Login.js:
  1. import { Form,Input} from "antd"; 
  2. import React from "react"; 
  3. //3 结构导航 
  4. import { Link,useNavigate } from "react-router-dom"; 
  5. import "../resources/authentication.css"; 
  6. //1 引入模块 
  7. import axios from 'axios' 
  8.  
  9. function Login() { 
  10.  
  11. //4 实例导航hooks 
  12. const navigate = useNavigate(); 
  13. const onFinish = async (values) => { 
  14. //2 发起请求 
  15. try { 
  16. const response = await axios.post("/api/users/login", values); 
  17. //3 本地存储 
  18. localStorage.setItem( 
  19. "expense-tracker-user", 
  20. JSON.stringify(response) 
  21. ); 
  22. message.success("登录成功!"); 
  23. // 5 导航进入主页 
  24. navigate("/"); 
  25. } catch (error) { 
  26. message.error("登录失败!"); 
  27. } 
  28. }; 
  29.  
  30. return  
  31. ... 
  32.  
  33. } 
  34.  
  35. export default Login; 
  1. 测试:注册新用户,查询MongoDB Compass数据库是否更新
  2. 测试:登录用户
  3. 查看Application中的LocalStorage是否保存数据
  4. 本地存储中,会保存密码,解决方式:
  1. const onFinish = async (values) => { 
  2.  
  3. try { 
  4. const response = await axios.post("/api/users/login", values); 
  5. localStorage.setItem( 
  6. "expense-tracker-user", 
  7. //解决方法 
  8. JSON.stringify({ ...response.data, password: "" }) 
  9. ); 
  10. message.success("登录成功!"); 
  11. navigate("/"); 
  12. } catch (error) { 
  13. message.error("登录失败!"); 
  14. } 
  15. }; 

五、加载/路由保护

1、加载和通知

  1. 使用antd提供的spin组件实现加载效果,选择spin state状态来进行控制;
  2. 所以进入Login.js,创建state状态:
  1. import React, { useEffect, useState } from "react"; 
  2.  
  3. function Login() { 
  4. const [loading, setLoading] = useState(false); 
  5. const navigate = useNavigate(); 
  6.  
  7. const onFinish = async (values) => { 
  8. try { 
  9. setLoading(true) 
  10. await axios.post("/api/users/login", values); 
  11. localStorage.setItem( 
  12. "expense-tracker-user", 
  13. //解决方法 
  14. JSON.stringify({ ...response.data, password: "" }) 
  15. ); 
  16. setLoading(false) 
  17. message.success("登录成功!"); 
  18. navigate("/"); 
  19. } catch (error) { 
  20. setLoading(false) 
  21. message.error("登录失败!"); 
  22. } 
  23. }; 
  1. 创建src/components/Spinner.js:
  1. import React from "react"; 
  2. import { Spin } from "antd"; 
  3.  
  4. function Spinner() { 
  5. return ( 
  6. <div > 
  7. <Spin/> 
  8. </div> 
  9. ); 
  10. } 
  11.  
  12. export default Spinner; 
  13.  
  1. 进入Login.js,插入Spinner组件,并将状态修改为true,进行测试:
  1. import Spinner from "../components/Spinner"; 
  2. ... 
  3. const [loading, setLoading] = useState(true); 
  4. return ( 
  5. <div className="register"> 
  6. {loading && <Spinner />} 
  7. ... 
  1. 添加Spinner组件样式,进入default-layout.css:
  1. .spinner{ 
  2. position: absolute; 
  3. top: 50%; 
  4. left: 50%; 
  5. transform: translate(-50% , -50%); 
  6. } 
  7. .ant-spin-dot-item{ 
  8. background-color: gray !important; 
  9. } 

6.进入Spinner.js:

  1. import React from "react"; 
  2. import { Spin } from "antd"; 
  3.  
  4. function Spinner() { 
  5. return ( 
  6. <div className="spinner"> 
  7. <Spin color='gray' style={{color:'gray'}} size='large'/> 
  8. </div> 
  9. ); 
  10. } 
  11.  
  12. export default Spinner; 
  13.  
  1. 将spinner效果粘贴到Register组件中:
  1. import { Form,Input} from "antd"; 
  2. import React, { useEffect, useState } from "react"; 
  3. import { Link } from "react-router-dom"; 
  4. import Spinner from "../components/Spinner"; 
  5. import "../resources/authentication.css"; 
  6. import axios from 'axios' 
  7.  
  8. function Register() { 
  9. const [loading, setLoading] = useState(false); 
  10. const navigate = useNavigate(true); 
  11. const onFinish = async (values) => { 
  12. try { 
  13. setLoading(true); 
  14. await axios.post("/api/users/register", values); 
  15. message.success("注册成功!"); 
  16. setLoading(false); 
  17. } catch (error) { 
  18. message.error("抱歉,出错了!"); 
  19. setLoading(false); 
  20. } 
  21. }; 
  22.  
  23. return ( 
  24. {loading && <Spinner />} 
  25. ... 
  26.  
  27. } 
  28.  
  29. export default Register; 

2、路由守卫

  1. 如果删除LocalStorage中保存的注册用户数据,你会发现直接请求根路径也是可以进入登录成功后的主页的,因此我们需要添加路由守卫,只有登录成功才能进入主页
  2. 进入App.js:
  1. import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; 
  2.  
  3. ... 
  4. <Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} /> 
  5. <Route path="/test" element={<ProtectedRoute><Test /></ProtectedRoute>} /> 
  6. ... 
  7.  
  8.  
  9.  
  10. export function ProtectedRoute(props){ 
  11.  
  12. if(localStorage.getItem('expense-tracker-user')) 
  13. { 
  14. return props.children 
  15. }else{ 
  16. return <Navigate to='/login'/> 
  17. } 
  18.  
  19. } 
  1. 进入Login.js:
  1. useEffect(() => { 
  2. if (localStorage.getItem("expense-tracker-user")) { 
  3. navigate("/"); 
  4. } 
  5. }, []); 
  1. 进入Register.js:
  1. import { Link, useNavigate } from "react-router-dom"; 
  2.  
  3. const navigate = useNavigate(true); 
  4.  
  5. useEffect(() => { 
  6. if (localStorage.getItem("expense-tracker-user")) { 
  7. navigate("/"); 
  8. } 
  9. }, []); 
  1. 进入DefaultLayout.js,获取username:
  1. const user = JSON.parse(localStorage.getItem("expense-tracker-user")); 
  2.  
  3. ... 
  4. <div>  
  5. <button className='primary'>{user.name}</button> 
  6.  
  7. </div> 
  8. ... 
  1. 进入antd, 拷贝dropdown组件代码,添加dropdown下拉式组件到DefaultLayout中:
  1. import React from "react"; 
  2. import { Menu, Dropdown, Button, Space } from "antd"; 
  3. import {useNavigate} from 'react-router-dom' 
  4.  
  5. import "../resources/default-layout.css"; 
  6.  
  7. function DefaultLayout(props) { 
  8. const user = JSON.parse(localStorage.getItem("expense-tracker-user")); 
  9. const navigate = useNavigate() 
  10. const items = [ 
  11. { 
  12. key:'1' 
  13. label: ( 
  14. <li onClick={()=>{ 
  15. localStorage.removeItem('expense-tracker-user') 
  16. navigate("/login"); 
  17. }}>退出</li> 
  18. ), 
  19. } 
  20. ] 
  21. return ( 
  22. <div className="layout"> 
  23. <div className="header d-flex justify-content-between align-items-center"> 
  24. <div> 
  25. <h1 className="logo">我的记账本</h1> 
  26. </div> 
  27. <div> 
  28. <Dropdown menu={{items}} placement="bottomLeft"> 
  29. <button className='primary'>{user.name}</button> 
  30. </Dropdown> 
  31. </div> 
  32. </div> 
  33.  
  34. <div className="content">{props.children}</div> 
  35. </div> 
  36. ); 
  37. } 
  38.  
  39. export default DefaultLayout; 
  40.  

六、添加流水UI

1、添加流水弹出框UI

  1. 本章节我们将创建添加流水的UI,先简单做个结构分析:
  2. 创建src/resources/transaction.css:
  1. .filter{ 
  2. box-shadow: 0 0 2px gray; 
  3. padding: 15px 20px; 
  4. border-radius: 5px; 
  5. } 
  6.  
  1. 进入Home.js组件:
  1. import axios from "axios"; 
  2. import React, { useEffect, useState } from "react"; 
  3. import "../resources/transactions.css"; 
  4. import { Modal } from 'antd'; 
  5.  
  6. function Home() { 
  7. //3 设置流水模式状态 
  8. const [showAddEditTransactionModal, setShowAddEditTransactionModal] = 
  9. useState(false); 
  10. return ( 
  11. <DefaultLayout> 
  12. {/* 1 上方过滤和类型切换按钮*/} 
  13. <div className="filter d-flex justify-content-between align-items-center"> 
  14. {/*左侧下拉框*/} 
  15. <div > 
  16.  
  17. <div> 
  18.  
  19. {/*右侧按钮*/} 
  20. <div > 
  21. {/*4 点击添加流水,会弹出模型框*/} 
  22. <button className="primary" onClick={() => setShowAddEditTransactionModal(true)}> 
  23. 添加流水 
  24. </button> 
  25.  
  26. </div> 
  27. </div> 
  28.  
  29. {/* 2 下方表格分析*/}  
  30. <div className="table-analtics"> 
  31.  
  32. </div> 
  33.  
  34. {/* 5 下方表格分析*/}  
  35. <Modal title="添加流水" open={showAddEditTransactionModal} onCancel=  
  36. {()=>setShowAddEditTransactionModal(false)}> 
  37. </Modal> 
  38. </DefaultLayout> 
  39. ); 
  40. } 
  41.  
  42. export default Home; 
  43.  
  1. 测试

2、添加流水表单UI

  1. 进入Home.js:
  1. import axios from "axios"; 
  2. import React, { useEffect, useState } from "react"; 
  3. import { Form, Input, Modal, Select } from "antd"; 
  4. import "../resources/transactions.css"; 
  5. import { Modal } from 'antd'; 
  6.  
  7. function Home() { 
  8. const [showAddEditTransactionModal, setShowAddEditTransactionModal] = 
  9. useState(false); 
  10.  
  11. // 5监听表单提交 
  12. const onFinish = (values) => { 
  13. console.log(values) 
  14. }; 
  15.  
  16. return ( 
  17. <DefaultLayout> 
  18.  
  19. <div className="filter d-flex justify-content-between align-items-center"> 
  20. {/*左侧下拉框*/} 
  21. <div > 
  22. <div> 
  23.  
  24. {/*右侧按钮*/} 
  25. <div > 
  26. <button className="primary" onClick={() => setShowAddEditTransactionModal(true)}> 
  27. 添加流水 
  28. </button> 
  29. <div> 
  30. </div> 
  31.  
  32. <div className="table-analtics"> 
  33. </div> 
  34. <Modal title="添加流水" open={showAddEditTransactionModal} onCancel=  
  35. {()=>setShowAddEditTransactionModal(false)} 
  36. {/*2 取消默认的按钮*/} 
  37. footer={false} 
  38. > 
  39. {/*1 构建模型框的内部form表格*/} 
  40. <Form 
  41. layout="vertical" 
  42. className="transaction-form" 
  43. {/*4 监听表单提交*/} 
  44. onFinish={onFinish}  
  45. > 
  46. <Form.Item label="金额" name="amount"> 
  47. <Input type="text" /> 
  48. </Form.Item> 
  49.  
  50. <Form.Item label="类型" name="type"> 
  51. <Select  
  52. initialValue='收入'  
  53. options={[ 
  54. { 
  55. value:'income', 
  56. label:'收入', 
  57. }, 
  58. { 
  59. value:'expense', 
  60. label:'支出', 
  61. } 
  62. ]} 
  63. /> 
  64.  
  65. </Form.Item> 
  66.  
  67. <Form.Item label="分类" name="category"> 
  68. <Select defaultValue="工资" 
  69. options={[ 
  70. { 
  71. value:'salary', 
  72. label:'工资', 
  73. }, 
  74. { 
  75. value:'freelance', 
  76. label:'兼职', 
  77. }, 
  78. { 
  79. value:'food', 
  80. label:'饮食', 
  81. }, 
  82. { 
  83. value:'entertainment', 
  84. label:'娱乐', 
  85. }, 
  86. { 
  87. value:'investment', 
  88. label:'投资', 
  89. }, 
  90. { 
  91. value:'travel', 
  92. label:'旅行', 
  93. }, 
  94. { 
  95. value:'education', 
  96. label:'教育', 
  97. }, 
  98. { 
  99. value:'medical', 
  100. label:'医疗', 
  101. }, 
  102. { 
  103. value:'tax', 
  104. label:'交通', 
  105. }, 
  106. ]} 
  107.  
  108. /> 
  109. </Form.Item> 
  110.  
  111. <Form.Item label="时间" name="date"> 
  112. <Input type="date" /> 
  113. </Form.Item> 
  114. {/**/} 
  115. <Form.Item label="关联" name="reference"> 
  116. <Input type="text" /> 
  117. </Form.Item> 
  118.  
  119. <Form.Item label="描述" name="description"> 
  120. <Input type="text" /> 
  121. </Form.Item> 
  122.  
  123. {/*3 自定义footer*/} 
  124. <div className="d-flex justify-content-end"> 
  125. <button className="primary" type="submit"> 
  126. 保存 
  127. </button> 
  128. </div> 
  129. </Form> 
  130. </Modal> 
  131. </DefaultLayout> 
  132. ); 
  133. } 
  134.  
  135. export default Home; 
  136.  
  137.  
  1. transaction.css样式:
  1. .filter{ 
  2. box-shadow: 0 0 2px gray; 
  3. padding: 15px 20px; 
  4. border-radius: 5px; 
  5. } 
  6.  
  7. .transaction-form label{ 
  8. color: rgba(0, 0, 0, 0.77) !important; 
  9. } 
  10. .transaction-form input{ 
  11. border: 1px solid gray; 
  12. border-bottom: 1px solid gray; 
  13. box-shadow: none !important; 
  14. } 
  15. .transaction-form input:focus{ 
  16. border: 1px solid gray; 
  17. border-bottom: 1px solid gray !important; 
  18. box-shadow: none !important; 
  19. } 
  20.  
  21. .ant-select-selector{ 
  22. border: 1px solid gray !important; 
  23. } 
  24. .ant-select-selector:focus{ 
  25. box-shadow:none !important; 
  26. outline: none !important; 
  27. } 
  28.  

七、添加流水API

1、添加流水模型

  1. 创建流水模型,创建src/models/Transaction.js:
  1. const mongoose = require("mongoose"); 
  2.  
  3. const transactionSchema = new mongoose.Schema({ 
  4. userid : { type: String, required: true}, 
  5. amount: { type: Number, required: true }, 
  6. type: { type: String, required: true }, 
  7. category: { type: String, required: true }, 
  8. reference: { type: String, required: true }, 
  9. description: { type: String, required: true }, 
  10. date: { type: Date, required: true }, 
  11. }); 
  12.  
  13. const transactionModel = mongoose.model("Trasactions", transactionSchema); 
  14.  
  15. module.exports = transactionModel; 
  16.  

2、添加流水API

  1. 创建src/routes/transactionsRoute.js:
  1. const express = require("express"); 
  2. const Transaction = require("../models/Transaction"); 
  3. const router = express.Router(); 
  4.  
  5. //添加流水 
  6. router.post("/add-transaction", async (req, res) { 
  7. try { 
  8. const newtransaction = new Transaction(req.body); 
  9. await newtransaction.save(); 
  10. res.send("流水添加成功!"); 
  11. } catch (error) { 
  12. res.status(500).json(error); 
  13. } 
  14. }); 
  15.  
  16. //获取所有流水 
  17. router.post("/get-all-transactions", async (req, res) { 
  18. try { 
  19. const transactions = await Transaction.find({userid:req.body.userid}); 
  20. res.send(transactions); 
  21. } catch (error) { 
  22. res.status(500).json(error); 
  23. } 
  24. }); 
  25.  
  1. 进入server.js:
  1. const transactionsRoute = require('./routes/transactionsRoute') 
  2. app.use('/api/transactions/' , transactionsRoute) 
  1. 创建src/components/AddEditTransaction.js组件单独保存Modal:
  1. import React, { useState } from "react"; 
  2. import { Form, Input, Modal, Select } from "antd"; 
  3.  
  4. function AddEditTransaction() { 
  5.  
  6. return ( 
  7. <Modal>...</Modal> 
  8.  
  9. ); 
  10. } 
  11.  
  12. export default AddEditTransaction; 
  13.  
  1. 进入Home.js,引入AddEditTransaction组件并传值:
  1. import AddEditTransaction from '../components/AddEditTransaction'; 
  2.  
  3.  
  4. {showAddEditTransactionModal && ( 
  5. <AddEditTransaction 
  6. showAddEditTransactionModal={showAddEditTransactionModal} 
  7. setShowAddEditTransactionModal={setShowAddEditTransactionModal} 
  8. /> 
  9. )} 
  10. </DefaultLayout> 
  1. 进入AddEditTransaction.js, 解构props及剪切onFinish:
  1. import React, { useState } from "react"; 
  2. import { Form, Input,message, Modal, Select } from "antd"; 
  3. import axios from "axios"; 
  4. import Spinner from "../components/Spinner"; 
  5.  
  6. function AddEditTransaction({  
  7. setShowAddEditTransactionModal, 
  8. showAddEditTransactionModal,}) { 
  9. //2 添加加载状态 
  10. const [loading, setLoading] = useState(false); 
  11. const navigate = useNavigate(); 
  12.  
  13. // 1设置onFinish 
  14. const onFinish = async (values) => { 
  15. try { 
  16. const user = JSON.parse(localStorage.getItem('expense-tracker-user')) 
  17. setLoading(true) 
  18. await axios.post("/api/transactions/add-transaction", {...values,userid:user._id}); 
  19. setShowAddEditTransactionModal(false) 
  20. setLoading(false) 
  21. message.success("流水添加成功!"); 
  22. } catch (error) { 
  23. setLoading(false) 
  24. message.error("抱歉,出错了!"); 
  25. } 
  26. }; 
  27.  
  28. return ( 
  29. <Modal> 
  30. {/*3 加载组件*/} 
  31. {loading && <Spinner />} 
  32. ...</Modal> 
  33.  
  34. ); 
  35. } 
  36.  
  37. export default AddEditTransaction; 
  1. 测试

3、展示流水数据

  1. 在antd中搜索table表格组件,简单查看代码结构;
  2. 进入Home.js:
  1. import Spinner from '../components/Spinner'; 
  2. import React, { useEffect, useState } from 'react'; 
  3.  
  4.  
  5. //3 添加loading状态  
  6. const [loading, setLoading] = useState(false); 
  7. //4 初始化流水状态 
  8. const [transactionsData, setTransactionsData] = useState([]); 
  9.  
  10. //2 创建获取所有流水的方法 
  11. const getTransactions = async () => { 
  12. try { 
  13. const user = JSON.parse(localStorage.getItem('expense-tracker-user')); 
  14. setLoading(true); 
  15. const response = await axios.post( 
  16. '/api/transactions/get-all-transactions', 
  17. { 
  18. userid: user._id, 
  19. } 
  20. ); 
  21. //6打印测试 
  22. console.log(response.data) 
  23. //5 存储流水 
  24. setTransactionsData(response.data); 
  25. setLoading(false); 
  26. } catch (error) { 
  27. setLoading(false); 
  28. message.error('抱歉,出错了!'); 
  29. } 
  30. }; 
  31. // 1 设置useEffect钩子函数 
  32. useEffect(() => { 
  33. getTransactions(); 
  34. }, []); 
  35.  
  36.  
  37. return 
  38. <DefaultLayout> 
  39. {loading && <Spinner />} 
  40. <DefaultLayout/> 
  41.  
  1. 测试,如果能够得到从服务器返回的流水数组,下一步就设置表格样式进行展示;
  2. 进入Home.js:
  1. import {Form, message, Select, Table } from 'antd'; 
  2. //2格式化组件库 
  3. import moment from 'moment'; 
  4. useEffect(() => { 
  5. getTransactions(); 
  6. }, []); 
  7. //1创建表格的columns 
  8. const columns = [ 
  9. { 
  10. title: '日期', 
  11. key: 'date', 
  12. dataIndex: 'date', 
  13. render: (text) => <span>{moment(text).format('YYYY-MM-DD')}</span>, 
  14. }, 
  15. { 
  16. title: '金额', 
  17. key: 'amount', 
  18. dataIndex: 'amount', 
  19. }, 
  20. { 
  21. title: '分类', 
  22. key: 'category', 
  23. dataIndex: 'category', 
  24. }, 
  25. { 
  26. title: '类型', 
  27. key: 'type', 
  28. dataIndex: 'type', 
  29. }, 
  30. { 
  31. title: '关联', 
  32. key: 'reference', 
  33. dataIndex: 'reference', 
  34. }, 
  35. ]; 
  36.  
  37. return ( 
  38. <DefaultLayout> 
  39. .... 
  40. <div className='table-analtics'> 
  41. {/*3引入table组件进行展示*/} 
  42. <div className='table'> 
  43. <Table columns={columns} dataSource={transactionsData} /> 
  44. </div> 
  45. </div> 
  46. {/*4每次添加重新请求所有交易流水,因此传递方法*/} 
  47. {showAddEditTransactionModal && ( 
  48. <AddEditTransaction 
  49. showAddEditTransactionModal={showAddEditTransactionModal} 
  50. setShowAddEditTransactionModal={setShowAddEditTransactionModal} 
  51. getTransactions={getTransactions} 
  52. /> 
  53. )} 
  54. </DefaultLayout> 
  55. ); 
  56. } 
  57.  
  58. export default Home; 
  1. 进入AddEditTransaction组件:
  1. import React, { useState } from "react"; 
  2. import { Form, Input,message, Modal, Select } from "antd"; 
  3. import axios from "axios"; 
  4. import Spinner from "../components/Spinner"; 
  5. import { v4 as uuidv4 } from 'uuid'; 
  6.  
  7. function AddEditTransaction({  
  8. setShowAddEditTransactionModal, 
  9. showAddEditTransactionModal, 
  10. getTransactions, 
  11.  
  12. }) {  
  13. const [loading, setLoading] = useState(false); 
  14. const navigate = useNavigate(); 
  15.  
  16. const onFinish = async (values) => { 
  17. try { 
  18. const user = JSON.parse(localStorage.getItem('expense-tracker-user')) 
  19. setLoading(true) 
  20. await axios.post("/api/transactions/add-transaction", {...values,userid:user._id},key: uuidv4(),); 
  21. //请求所有交易流水 
  22. getTransactions(); 
  23. message.success("流水添加成功!"); 
  24. setShowAddEditTransactionModal(false); 
  25. setLoading(false); 
  26. } catch (error) { 
  27. setLoading(false) 
  28. message.error("抱歉,出错了!"); 
  29. } 
  30. }; 
  31.  
  32. return ( 
  33. <Modal> 
  34. {loading && <Spinner />} 
  35. ...</Modal> 
  36.  
  37. ); 
  38. } 
  39.  
  40. export default AddEditTransaction; 
  41.  
  42.  
  1. 在client中安装uuid,moment组件
  1. npm install uuid moment 

八、过滤功能

1、解释日期过滤

  1. 首先确保构建的Transition Schema添加了date字段及数据结构。
  2. 添加一些虚拟的交易流水;因为过滤日期具体分为最近一周、最近一个月,最近一年,所以尝试添加上一周的数据,上一个月的数据;
  3. 进入transactionsRoute.js,设置过滤API:
  1. //获取所有流水(过滤) 
  2. //(>) 大于 - $gt 
  3. //(<) 小于 - $lt 
  4. //(>=) 大于等于 - $gte 
  5. //(<= ) 小于等于 - $lte 
  6.  
  7. //Moment.js是一个轻量级的JavaScript时间库,以前我们转化时间,都会进行很复杂的操作,而Moment.js的出现,简化了我们开发中对时间的处理,提高了开发效率。日常开发中,通常会对时间进行下面这几个操作:比如获取时间,设置时间,格式化时间,比较时间等等。 
  8. //https://blog.csdn.net/weixin_43923808/article/details/126233378 
  9.  
  10. router.post("/get-all-transactions", async (req, res) { 
  11. try { 
  12. const transactions = await Transaction.find( 
  13. { 
  14. //获取2022-05-01之后的数据,并转化date对象 
  15. date:{ 
  16. $gt:moment('2022-05-01').toDate(); 
  17. //获取2022-05-01之前的数据,并转化date对象 
  18. $lt:moment('2022-04-01').toDate(); 
  19. }, 
  20. userid:req.body.userid 
  21. }); 
  22. res.send(transactions); 
  23. } catch (error) { 
  24. res.status(500).json(error); 
  25. } 
  26. }); 
  1. 如何实现查询最近一周,一个月,一年的数据?
  1. router.post("/get-all-transactions", async (req, res) { 
  2. try { 
  3. const transactions = await Transaction.find( 
  4. { 
  5.  
  6. date:{ 
  7. //获取最近一周数据 
  8. $gt:moment().substract(7,'d').toDate(); 
  9. //获取最近一月数据 
  10. $gt:moment().substract(30,'d').toDate(); 
  11. }, 
  12. userid:req.body.userid 
  13. }); 
  14. res.send(transactions); 
  15. } catch (error) { 
  16. res.status(500).json(error); 
  17. } 
  18. }); 
  1. 所以可以根据不同的日期频率来过滤交易流水,进行页面渲染。

2、实现日期过滤

  1. 首先创建过滤组件UI,进入Home.js:
  1. import axios from "axios"; 
  2. import React, { useEffect, useState } from "react"; 
  3. import { Form, Input, Modal, Select } from "antd"; 
  4. import "../resources/transactions.css"; 
  5. import { Modal } from 'antd'; 
  6.  
  7. function Home() { 
  8. const [showAddEditTransactionModal, setShowAddEditTransactionModal] = 
  9. useState(false); 
  10. const [loading, setLoading] = useState(false); 
  11. const [transactionsData, setTransactionsData] = useState([]); 
  12. //2 创建日期频率状态 
  13. const [frequency,setFrequency] = useState('7') 
  14.  
  15. const getTransactions = async () => { 
  16. try { 
  17. const user = JSON.parse(localStorage.getItem('expense-tracker-user')); 
  18. setLoading(true); 
  19. //3添加日期频率请求 
  20. const response = await axios.post( 
  21. '/api/transactions/get-all-transactions', 
  22. { 
  23. userid: user._id, 
  24. frequency, 
  25. } 
  26. ); 
  27. console.log(response.data) 
  28. setTransactionsData(response.data); 
  29. setLoading(false); 
  30. } catch (error) { 
  31. setLoading(false); 
  32. message.error('抱歉,出错了!'); 
  33. } 
  34. }; 
  35.  
  36. useEffect(() => { 
  37. getTransactions(); 
  38. }, []); 
  39.  
  40. return ( 
  41. <DefaultLayout> 
  42.  
  43. <div className="filter d-flex justify-content-between align-items-center"> 
  44. {/*1左侧过滤下拉框*/} 
  45. <div class='d-flex flex-column' > 
  46. <h6>选择日期y</h6> 
  47. <Select value={frequency} onChange={(value) => setFrequency(value)} 
  48. options={[ 
  49. { 
  50. value:'7', 
  51. label:'最近一周', 
  52. }, 
  53. { 
  54. value:'30', 
  55. label:'最近一月', 
  56. }, 
  57. { 
  58. value:'365', 
  59. label:'最近一年', 
  60. }, 
  61. { 
  62. value:'custom', 
  63. label:'自定义', 
  64. }, 
  65. ]} 
  66.  
  67. /> 
  68.  
  69. <div> 
  70.  
  71. ... 
  72. </div> 
  73. </Form> 
  74. </Modal> 
  75. </DefaultLayout> 
  76. ); 
  77. } 
  78.  
  79. export default Home; 
  80.  
  81.  
  82.  
  1. 进入transactionRoute.js,获取前端发送的日期频率
  1. router.post("/get-all-transactions", async (req, res) { 
  2. try { 
  3. const transactions = await Transaction.find( 
  4. { 
  5. //获取日期频率 
  6. date:{ 
  7. $gt:moment().substract(Number(req.body.frequency),'d').toDate(); 
  8. }, 
  9. userid:req.body.userid 
  10. }); 
  11. res.send(transactions); 
  12. } catch (error) { 
  13. res.status(500).json(error); 
  14. } 
  15. }); 
  1. 在Home.js中添加useEffect的依赖项,数据更新重新渲染UI:
  1. useEffect(() => { 
  2. getTransactions(); 
  3. }, [frequency]); 
  1. 在Home.js的选择日期样式下面, 添加自定义日期的UI样式:
  1. //DatePicker日期选择框约束开始结束时间  
  2. import { DatePicker, Space } from 'antd'; 
  3. const { RangePicker } = DatePicker; 
  4.  
  5. {frequency === 'custom' && ( 
  6. <RangePicker/> 
  7. )} 
  1. 在Home.js中保存选择的自定义日期状态:
  1. const [selectedRange, setSelectedRange] = useState([]); 
  2.  
  3. {frequency === 'custom' && ( 
  4. <div className='mt-2'> 
  5. <RangePicker 
  6. value={selectedRange} 
  7. onChange={(values) => setSelectedRange(values)} 
  8. /> 
  9. </div> 
  10. )} 
  11.  
  1. 在Home.js中添加自定义请求:
  1. const getTransactions = async () => { 
  2. try { 
  3. const user = JSON.parse(localStorage.getItem('expense-tracker-user')); 
  4. setLoading(true); 
  5.  
  6. const response = await axios.post( 
  7. '/api/transactions/get-all-transactions', 
  8. { 
  9. userid: user._id, 
  10. frequency, 
  11. //1添加日期频率请求 
  12. ...(frequency === 'custom' && { selectedRange }), 
  13. } 
  14. ); 
  15. console.log(response.data) 
  16. setTransactionsData(response.data); 
  17. setLoading(false); 
  18. } catch (error) { 
  19. setLoading(false); 
  20. message.error('抱歉,出错了!'); 
  21. } 
  22. }; 
  23. //2 
  24. useEffect(() => { 
  25. getTransactions(); 
  26. }, [frequency,selectedRange]); 
  27.  
  1. 进入后台transaction.js进行获取:
  1. router.post("/get-all-transactions", async (req, res) => { 
  2. //获取body中的不同值 
  3. const { frequency, selectedRange } = req.body; 
  4. try { 
  5. const transactions = await Transaction.find({ 
  6. ...(frequency !== "custom" 
  7. ? { 
  8. date: { 
  9. $gt: moment().subtract(Number(req.body.frequency), "d").toDate(), 
  10. }, 
  11. } 
  12. : { 
  13. date: { 
  14. $gte: selectedRange[0], 
  15. $lte: selectedRange[1], 
  16. }, 
  17. }), 
  18. userid: req.body.userid, 
  19. }); 
  20.  
  21. res.send(transactions); 
  22. } catch (error) { 
  23. res.status(500).json(error); 
  24. } 
  25. }); 

3、类型过滤

  1. 进入Home.js,拷贝上方选择日期的结构进行粘贴修改:
  1. const [type, setType] = useState('all'); 
  2.  
  3.  
  4. <div className='d-flex'> 
  5. ... 
  6. <div class='d-flex flex-column mx-5' > 
  7. <h6>选择类型</h6> 
  8. <Select value={type} onChange={(value) => setType(value)} 
  9. options={[ 
  10. { 
  11. value:'all', 
  12. label:'所有类型', 
  13. }, 
  14. { 
  15. value:'income', 
  16. label:'收入', 
  17. }, 
  18. { 
  19. value:'expense', 
  20. label:'支出', 
  21. }, 
  22. ]} 
  23.  
  24. /> 
  25.  
  26. <div> 
  27. </div> 
  1. 在请求中添加type:
  1. const getTransactions = async () => { 
  2. try { 
  3. const user = JSON.parse(localStorage.getItem('expense-tracker-user')); 
  4. setLoading(true); 
  5.  
  6. const response = await axios.post( 
  7. '/api/transactions/get-all-transactions', 
  8. { 
  9. userid: user._id, 
  10. frequency, 
  11. ...(frequency === 'custom' && { selectedRange }), 
  12. //1添加类型请求 
  13. type 
  14. } 
  15. ); 
  16. console.log(response.data) 
  17. setTransactionsData(response.data); 
  18. setLoading(false); 
  19. } catch (error) { 
  20. setLoading(false); 
  21. message.error('抱歉,出错了!'); 
  22. } 
  23. }; 
  24. //2 
  25. useEffect(() => { 
  26. getTransactions(); 
  27. }, [frequency,selectedRange,type]); 
  1. 进入transaction.js:
  1. router.post("/get-all-transactions", async (req, res) => { 
  2. const { frequency, selectedRange , type } = req.body; 
  3. try { 
  4. const transactions = await Transaction.find({ 
  5. ...(frequency !== "custom" 
  6. ? { 
  7. date: { 
  8. $gt: moment().subtract(Number(req.body.frequency), "d").toDate(), 
  9. }, 
  10. } 
  11. : { 
  12. date: { 
  13. $gte: selectedRange[0], 
  14. $lte: selectedRange[1], 
  15. }, 
  16. }), 
  17. userid: req.body.userid, 
  18. //1查找type 
  19. ...(type!=='all' && {type}) 
  20. }); 
  21.  
  22. res.send(transactions); 
  23. } catch (error) { 
  24. res.status(500).json(error); 
  25. } 
  26. }); 

九、数据分析

1、添加视图切换组件

  1. 进入Home.js,在filter样式的下方,创建分析组件:
  1. //2 
  2. import { 
  3. UnorderedListOutlined, 
  4. AreaChartOutlined, 
  5. EditOutlined, 
  6. DeleteOutlined, 
  7. } from '@ant-design/icons';  
  8.  
  9. function Home() { 
  10. ... 
  11.  
  12. //3 创建显示状态 
  13. const [viewType, setViewType] = useState('table'); 
  14.  
  15. <div className='d-flex'> 
  16. <div> 
  17. <div className='view-switch mx-5'> 
  18. {/* 1 引入antd提供的icon图标,搜索list*/} 
  19. <UnorderedListOutlined 
  20. className={`mx-3 ${ 
  21. viewType === 'table' ? 'active-icon' : 'inactive-icon' 
  22. } `} 
  23. onClick={() => setViewType('table')} 
  24. size={30} 
  25. /> 
  26. <AreaChartOutlined 
  27. className={`${ 
  28. viewType === 'analytics' ? 'active-icon' : 'inactive-icon' 
  29. } `} 
  30. onClick={() => setViewType('analytics')} 
  31. size={30} 
  32. /> 
  33. </div> 
  34. </div> 
  35. <button 
  36. className='primary' 
  37. onClick={() => setShowAddEditTransactionModal(true)} 
  38. > 
  39. ADD NEW 
  40. </button> 
  41. </div> 
  42. ... 
  43. } 
  1. 进入transaction.css,添加样式:
  1.  
  2. .view-switch{ 
  3. border: 1px solid rgba(0, 0, 0, 0.71); 
  4. border-radius: 3px; 
  5. padding: 5px 10px; 
  6. } 
  7.  
  8. {/*3 添加样式*/} 
  9. .anticon svg{ 
  10. font-size: 20px; 
  11. cursor: pointer; 
  12. } 
  13.  
  14. .active-icon{ 
  15. color: black; 
  16.  
  17. } 
  18. .inactive-icon{ 
  19. color: gray; 
  20.  
  21. } 

2、总交易流水笔数分析

  1. 创建src/components/Analytics.js
  1. import React from 'react'; 
  2.  
  3. function Analytics({transactions}){ 
  4. return ( 
  5. <div>交易分析</div> 
  6. ) 
  7. } 
  8.  
  9. export default Analytics; 
  1. 进入Home.js,插入交易分析组件:
  1. <div className='table-analtics'> 
  2. {viewType === 'table' ? ( 
  3. <div className='table'> 
  4. <Table columns={columns} dataSource={transactionsData} /> 
  5. </div> 
  6. ) : ( 
  7. <Analytics transactions={transactionsData} /> 
  8. )} 
  9. </div> 
  1. 进入Analytics.js,获取支出流水和收入流水占总交易流水的比例:
  1. import React from 'react'; 
  2.  
  3. function Analytics({transactions}){ 
  4. //1计算比例 
  5. const totalTransactions = transactions.length; 
  6. const totalIncomeTransactions = transactions.filter( 
  7. (transaction) => transaction.type === "income" 
  8. ); 
  9. const totalExpenceTransactions = transactions.filter( 
  10. (transaction) => transaction.type === "expence" 
  11. ); 
  12. const totalIncomeTransactionsPercentage = 
  13. (totalIncomeTransactions.length / totalTransactions) * 100; 
  14. const totalExpenceTransactionsPercentage = 
  15. (totalExpenceTransactions.length / totalTransactions) * 100; 
  16.  
  17. return ( 
  18. //2创建样式 
  19. <div className='analytics'> 
  20. <div className="row"> 
  21. <div className="col-md-4 mt-3"> 
  22. <div className="transactions-count"> 
  23. <h4>总交易流水 : {totalTransactions}</h4> 
  24. <hr /> 
  25. <h5>收入 : {totalIncomeTransactions.length}</h5> 
  26. <h5>支出 : {totalExpenceTransactions.length}</h5> 
  27. </div> 
  28. </div> 
  29. </div> 
  30. </div> 
  31. ) 
  32. } 
  33.  
  34. export default Analytics; 
  1. 创建src/resources/analytics.css,并引入到analytics.js组件:
  1. .transactions-count{ 
  2. box-shadow: 0 0 2px rgb(132, 131, 131); 
  3. padding: 15px; 
  4. border-radius: 10px; 
  5. color: rgb(56, 55, 55) !important; 
  6. } 
  7.  
  8. .analytics h4{ 
  9. font-size: 20px; 
  10. color: gray !important; 
  11. font-weight: 600; 
  12. } 
  13. .analytics h5{ 
  14. font-size: 16px; 
  15. color: gray !important; 
  16. } 
  17.  
  1. 在antd中搜索Progress组件:
  1. import { Progress } from "antd"; 
  2.  
  3. return ( 
  4. <div className='analytics'> 
  5. <div className="row"> 
  6. <div className="col-md-4 mt-3"> 
  7. <div className="transactions-count"> 
  8. <h4>总交易流水 : {totalTransactions}</h4> 
  9. <hr /> 
  10. <h5>收入 : {totalIncomeTransactions.length}</h5> 
  11. <h5>支出 : {totalExpenceTransactions.length}</h5> 
  12.  
  13. {/*1 progress bar*/} 
  14. <div className="progress-bars"> 
  15. <Progress 
  16. className="mx-5" 
  17. strokeColor="#5DD64F" 
  18. type="circle" 
  19. percent={totalIncomeTransactionsPercentage.toFixed(0)} 
  20. /> 
  21. <Progress 
  22. strokeColor="#E5572F" 
  23. type="circle" 
  24. percent={totalExpenceTransactionsPercentage.toFixed(0)} 
  25. /> 
  26. </div> 
  27. </div> 
  28. </div> 
  29. </div> 
  30. </div> 
  31. ) 
  32. } 

3、总交易流水金额分析

  1. 进入Analytics.js文件,计算总交易金额:
  1. function Analatics({ transactions }) { 
  2. //1 总交易金额 
  3. const totalTurnover = transactions.reduce( 
  4. (acc, transaction) => acc + transaction.amount, 
  5. 0 
  6. ); 
  7. //2 总收入金额 
  8. const totalIncomeTurnover = transactions 
  9. .filter((transaction) => transaction.type === "income") 
  10. .reduce((acc, transaction) => acc + transaction.amount, 0); 
  11. //3 总支出金额 
  12. const totalExpenceTurnover = transactions 
  13. .filter((transaction) => transaction.type === "expence") 
  14. .reduce((acc, transaction) => acc + transaction.amount, 0); 
  15. console.log(totalExpenceTurnover); 
  16. //4 总收入金额占总交易金额的比例 
  17. const totalIncomeTurnoverPercentage = 
  18. (totalIncomeTurnover / totalTurnover) * 100; 
  19. //5 总支出金额占总交易金额的比例 
  20. const totalExpenceTurnoverPercentage = 
  21. (totalExpenceTurnover / totalTurnover) * 100; 
  22.  
  23.  
  24. return ( 
  25. ... 
  26. //6 构建结构 
  27. <div className="col-md-4 mt-3"> 
  28. <div className="transactions-count"> 
  29. <h4>总交易金额 : {totalTurnover}</h4> 
  30. <hr /> 
  31. <h5>总收入 : {totalIncomeTurnover}</h5> 
  32. <h5>总支出 : {totalExpenceTurnover}</h5> 
  33.  
  34. <div className="progress-bars"> 
  35. <Progress 
  36. className="mx-5" 
  37. strokeColor="#5DD64F" 
  38. type="circle" 
  39. percent={totalIncomeTurnoverPercentage.toFixed(0)} 
  40. /> 
  41. <Progress 
  42. strokeColor="#E5572F" 
  43. type="circle" 
  44. percent={totalExpenceTurnoverPercentage.toFixed(0)} 
  45. /> 
  46. </div> 
  47. </div> 
  48. </div> 
  49. ... 
  50. ) 
  51. } 
  1. 添加新交易测试

十、分类分析

1、收入类别分析

  1. 创建所有交易分类;
  2. 进入Analytics.js,创建分类的分析UI:
  1. const categories = [ 
  2. "salary", 
  3. "entertainment", 
  4. "freelance", 
  5. "food", 
  6. "travel", 
  7. "investment", 
  8. "education", 
  9. "medical", 
  10. "tax", 
  11. ]; 
  12.  
  13. <hr />  
  14. <div className="row"> 
  15. <div className="col-md-6"> 
  16. <div className="category-analysis"> 
  17. <h4>收入 - 类别分析</h4> 
  18. {categories.map((category) => { 
  19. const amount = transactions 
  20. .filter((t) => t.type == "income" && t.category === category) 
  21. .reduce((acc, t) => acc + t.amount, 0); 
  22. return ( 
  23. amount > 0 && <div className="category-card"> 
  24. <h5>{category}</h5> 
  25. <Progress strokeColor='#0B5AD9' percent={((amount / totalIncomeTurnover) * 100).toFixed(0)} /> 
  26. </div> 
  27. ); 
  28. })} 
  29. </div> 
  30. </div> 
  31. </div> 
  1. 进入analytics.css:
  1. .category-card{ 
  2. padding: 5px 20px; 
  3. box-shadow: 0 0 2px gray; 
  4. margin-top: 15px; 
  5. border-radius: 5px; 
  6. } 
  1. 进入default-layout.css,添加滚动条:
  1. .content{ 
  2. height: 85vh; 
  3. box-shadow: 0 0 2px gray; 
  4. margin-top: 20px; 
  5. border-top-right-radius: 25px; 
  6. border-top-left-radius: 25px; 
  7. padding: 15px; 
  8. overflow-y: scroll; 
  9. } 

2、支出类别分析

  1. 进入Analytics.js,只需要拷贝收入类型分析结构进行修改即可:
  1. <div className="col-md-6"> 
  2. <div className="category-analysis"> 
  3. <h4>支出 - 类别分析</h4> 
  4. {categories.map((category) => { 
  5. const amount = transactions 
  6. .filter((t) => t.type == "expence" && t.category === category) 
  7. .reduce((acc, t) => acc + t.amount, 0); 
  8. return ( 
  9. amount > 0 && <div className="category-card"> 
  10. <h5>{category}</h5> 
  11. <Progress strokeColor='#0B5AD9' percent={((amount / totalExpenceTurnover) * 100).toFixed(0)} /> 
  12. </div> 
  13. ); 
  14. })} 
  15. </div> 
  16. </div> 

十一、编辑或删除交易

1、编辑交易

  1. 进入Home.js,添加编辑/删除列:
  1.  
  2. //2引入组件 
  3. import { 
  4. UnorderedListOutlined, 
  5. AreaChartOutlined, 
  6. EditOutlined, 
  7. DeleteOutlined, 
  8. } from '@ant-design/icons'; 
  9.  
  10. function Home() { 
  11. ... 
  12. //3创建状态 
  13. const [selectedItemForEdit, setSelectedItemForEdit] = useState(null); 
  14. const columns = [ 
  15. { 
  16. title: '日期', 
  17. key: 'date', 
  18. dataIndex: 'date', 
  19. render: (text) => <span>{moment(text).format('YYYY-MM-DD')}</span>, 
  20. }, 
  21. { 
  22. title: '金额', 
  23. key: 'amount', 
  24. dataIndex: 'amount', 
  25. }, 
  26. { 
  27. title: '分类', 
  28. key: 'category', 
  29. dataIndex: 'category', 
  30. }, 
  31. { 
  32. title: '类型', 
  33. key: 'type', 
  34. dataIndex: 'type', 
  35. }, 
  36. { 
  37. title: '关联', 
  38. key: 'reference', 
  39. dataIndex: 'reference', 
  40. }, 
  41. //1添加编辑/删除列 
  42. { 
  43. title: '操作', 
  44. key:'actions' 
  45. dataIndex: 'actions', 
  46. render: (text, record) => { 
  47. return ( 
  48. <div> 
  49. {/*antd中查询editor,delete图标*/} 
  50. <EditOutlined 
  51. //4事件监听 
  52. onClick={() => { 
  53. setSelectedItemForEdit(record); 
  54. setShowAddEditTransactionModal(true); 
  55. }} 
  56. /> 
  57. <DeleteOutlined 
  58. className='mx-3' 
  59. onClick={() => deleteTransaction(record)} 
  60. /> 
  61. </div> 
  62. ); 
  63. }, 
  64. }, 
  65. ]; 
  66.  
  67. return( 
  68. ... 
  69. {showAddEditTransactionModal && ( 
  70. //5传递方法和状态 
  71. <AddEditTransaction 
  72. showAddEditTransactionModal={showAddEditTransactionModal} 
  73. setShowAddEditTransactionModal={setShowAddEditTransactionModal} 
  74. selectedItemForEdit={selectedItemForEdit} 
  75. getTransactions={getTransactions}  
  76. setSelectedItemForEdit={setSelectedItemForEdit} 
  77. /> 
  78. )} 
  79. ) 
  80.  
  81. } 
  82.  
  1. 进入AddEditTransaction组件:
  1. import React, { useState } from "react"; 
  2. import { Form, Input,message, Modal, Select } from "antd"; 
  3. import axios from "axios"; 
  4. import Spinner from "../components/Spinner"; 
  5. import { v4 as uuidv4 } from 'uuid'; 
  6.  
  7. function AddEditTransaction({  
  8. setShowAddEditTransactionModal, 
  9. showAddEditTransactionModal, 
  10. //1 解构props 
  11. selectedItemForEdit, 
  12. setSelectedItemForEdit, 
  13. getTransactions, 
  14.  
  15. }) {  
  16. const [loading, setLoading] = useState(false); 
  17. const navigate = useNavigate(); 
  18.  
  19. const onFinish = async (values) => { 
  20. try { 
  21. const user = JSON.parse(localStorage.getItem('expense-tracker-user')) 
  22. setLoading(true) 
  23. await axios.post("/api/transactions/add-transaction", {...values,userid:user._id},key: uuidv4(),); 
  24. getTransactions(); 
  25. message.success("流水添加成功!"); 
  26. setShowAddEditTransactionModal(false); 
  27.  
  28. setLoading(false); 
  29. } catch (error) { 
  30. setLoading(false) 
  31. message.error("抱歉,出错了!"); 
  32. } 
  33. }; 
  34.  
  35. return ( 
  36. //2修改modal title 
  37. <Modal 
  38. title={selectedItemForEdit ? "编辑交易流水" : "添加交易流水"} 
  39. visible={showAddEditTransactionModal} 
  40. onCancel={() => setShowAddEditTransactionModal(false)} 
  41. footer={false} 
  42. > 
  43. {loading && <Spinner />} 
  44. {/*3添加initialValues*/} 
  45. <Form 
  46. layout="vertical" 
  47. className="transaction-form" 
  48. onFinish={onFinish} 
  49. initialValues={selectedItemForEdit} 
  50. > 
  51. ...</Modal> 
  52.  
  53. ); 
  54. } 
  55.  
  56. export default AddEditTransaction; 
  57.  
  58.  
  59.  

2、实现编辑和删除交易

  1. 进入transactionRoute.js,创建编辑路由:
  1. router.post("/edit-transaction", async function (req, res) { 
  2. try { 
  3. await Transaction.findOneAndUpdate({_id : req.body.transactionId} , req.body.payload) 
  4. res.send("交易流水更新成功!"); 
  5. } catch (error) { 
  6. res.status(500).json(error); 
  7. } 
  8. }); 
  1. 进入AddEditTransaction.js:
  1. const onFinish = async (values) => { 
  2. try { 
  3. const user = JSON.parse(localStorage.getItem("expense-tracker-user")); 
  4. setLoading(true); 
  5. //1 判断编辑还是添加交易 
  6. if (selectedItemForEdit) { 
  7. await axios.post("/api/transactions/edit-transaction", { 
  8. payload : { 
  9. ...values, 
  10. userid: user._id, 
  11. }, 
  12. transactionId: selectedItemForEdit._id, 
  13. }); 
  14. getTransactions(); 
  15. message.success("交易流水更新成功!"); 
  16. } else { 
  17. await axios.post("/api/transactions/add-transaction", { 
  18. ...values, 
  19. userid: user._id, 
  20. }); 
  21. getTransactions(); 
  22. message.success("交易流水添加成功!"); 
  23. } 
  24. setShowAddEditTransactionModal(false); 
  25. //2还原状态为null 
  26. setSelectedItemForEdit(null) 
  27. setSelectedItemForEdit(null); 
  28. setLoading(false); 
  29. } catch (error) { 
  30. message.error("抱歉,出错了"); 
  31. setLoading(false); 
  32. } 
  33. }; 
  1. 进入transactionRoute.js,创建删除交易流水路由:
  1.  
  2. router.post("/delete-transaction", async function (req, res) { 
  3. try { 
  4. await Transaction.findOneAndDelete({_id : req.body.transactionId}) 
  5. res.send("Transaction Updated Successfully"); 
  6. } catch (error) { 
  7. res.status(500).json(error); 
  8. } 
  9. }); 
  1. 进入Home.js,创建删除交易事件:
  1. const deleteTransaction = async (record) => { 
  2. try { 
  3. setLoading(true); 
  4. await axios.post('/api/transactions/delete-transaction', { 
  5. transactionId: record._id, 
  6. }); 
  7. message.success('交易流水删除成功!'); 
  8. getTransactions(); 
  9. setLoading(false); 
  10. } catch (error) { 
  11. setLoading(false); 
  12. message.error('抱歉,出错了'); 
  13. } 
  14. }; 
  15.  
  16. const columns = [ 
  17. ... 
  18. { 
  19. title: '操作', 
  20. key:'actions' 
  21. dataIndex: 'actions', 
  22. render: (text, record) => { 
  23. return ( 
  24. <div> 
  25. <EditOutlined 
  26. onClick={() => { 
  27. setSelectedItemForEdit(record); 
  28. setShowAddEditTransactionModal(true); 
  29. }} 
  30. /> 
  31. <DeleteOutlined 
  32. className='mx-3' 
  33. onClick={() => deleteTransaction(record)} 
  34. /> 
  35. </div> 
  36. ); 
  37. }, 
  38. }, 

十二、重构和部署

1、样式微调

  1. 更新主题颜色:尝试更改default-layout的背景色及primary主题色,例如改为:#1b7e14等;
  2. 可以调整登录和注册页面的btn颜色,单独创建一个secondary的css类:
  1. .secondary{ 
  2. background-color: #ffffff; 
  3. padding:5px 20px; 
  4. color: black !important; 
  5. border: none; 
  6. } 
  1. 进入analytics.js,设置key props

  2. 进入transaction.css更改picker range效果,同时设置翻页导航字体颜色,并添加媒体查询:

  1. .ant-picker-range{ 
  2. border: 1px solid black !important; 
  3. } 
  4. .ant-pagination a { 
  5. color:rgba(0,0,0,0.77) !important; 
  6. } 
  7. @media screen and (max-width:600px){ 
  8. .filter{ 
  9. overflow-x: scroll; 
  10. } 
  11. } 
  1. 使用aos动画库:AOS(Animate on scroll)是小型动画滚动库,可在页面滚动时给元素添加动画效果。进入app.js文件
  1. import React, { useEffect } from "react"; 
  2. import AOS from "aos"; 
  3. import "aos/dist/aos.css"; 
  4. function App() { 
  5. useEffect(() => { 
  6. //如果你不想单独每个元素做一个动画配置,你可以通过init()方法来统一配置所有元素的动画效果。 
  7. //refresh()会重新计算元素的位置和偏移。 
  8. AOS.init({ 
  9. duration: 500, 
  10. easing: 'ease-in-back', 
  11. }); 
  12. AOS.refresh(); 
  13. }, []); 
  1. 进入Login.js,为组件设置data-aos值:
  1. <div className='col-md-4' data-aos='fade-right'> 
  2. ... 
  3. <div className='col-md-5' data-aos='fade-left'> 
  1. 进入Register.js,为组件设置data-aos值:
  1. <div className='col-md-5' data-aos='fade-right'> 
  2. ... 
  3. <div className='col-md-4' data-aos='fade-left'> 
  1. 修改title标题

2、React前后端分离部署 -Nodejs后端部署

  1. Nodejs后端部署

    1)准备工作:

    • 拆分前后端项目
    • expense-tracker⽂件名改名为7001
    • 修改server.js⽂件名为7001
    • 后端启动的服务较多, 所以统⼀⽤端⼝号来标识
    • 修改7001.js的端⼝号
    • 修改package.json的启动⽂件名7001.js
    • 删除node_modules⽂件夹

    2)登录宝塔,将后端7001项⽬⽂件夹放⼊服务器路径

    • ⽂件/www/wwwroot/www.thenewstep.cn/backend/

    3)安装项⽬依赖模块

    • 双击7001,进⼊项⽬⽂件路径
    • 点击终端

    4) 终端运⾏命令

    1. npm install 

    5)启动项⽬测试

    1. node 7001.js 
    • 启动正常,数据库连接正常, 但此时不能关闭终端, 关闭的话服务就断了
    • 所以不能使⽤node 7001.js启动
    • ctrl+c关闭服务,使⽤另外的命令启动

    6)使⽤pm2永久启动项⽬,PM2是常用的node进程管理工具,它可以提供node.js应用管理,如自动重载、性能监控、负载均衡等

    1. pm2 start 7001.js 
    • 启动好之后, 即可关闭终端

    7)配置反向代理

    • 确保当⽤户访问当前服务端⼝时, 可以指向正确的服务
    • 侧边栏找到⽹站-www.thenewstep.cn->反向代理

    8)添加反向代理

    • 选择【高级功能】,添加代理名称(expense-tracker),代理目录(/banckend/7001),目标URL(http://localhost:7001),进行提交。

    9) 重启nginx

    • 找到⽂件-> 终端 任意路径都可以,只要打开终端就⾏, 不是⾮要在7001⽂件路径下
    • 重启nginx命令
    1. service nginx reload 

    10)测试后端接⼝

    • 打开client项⽬, 更换接⼝地址
    • package.json
    1. "proxy": "https://www.thenewstep.cn/backend/7001" 

3、React前后端分离部署 -前端打包部署-阿⾥云

  1. client项⽬运⾏确保正常
  1. npm start 
  1. 修改package.json打包路径
  1. { 
  2. "name": "client", 
  3. "version": "0.1.0", 
  4. "private": true, 
  5. // 加⼊homepage字段-项目的主页地址, (.)表示当前文件夹下的相对路径 
  6. "homepage": ".", 
  7. "dependencies": { 
  8. ... 
  9. } 
  10. } 
  • 打包出来的静态⽂件, 会在路径前加⼀个点, 例如./static
  • 如果homepage的值是 "/xxx/xxx/" 那么打包的静态⽂件html中引⼊的路径就是/xxx/xxx/static
  1. 修改App.js路由根路径

    • 修改根路径的⽬的是为了确保跟服务器的⽂件路径⼀直
    • 保证刷新仍然可以找到静态⽂件
    1. <Routers basename="/frontend/react/7001"> 
  2. 配置后端接⼝根路径

  • 打包之后,反向代理失效
  • 所以为了确保请求接⼝地址的完整
  • 就需要配置axios请求的根路径
  • Index.js
  1. import axios from 'axios'; 
  2.  
  3. axios.defaults.baseURL = 'https://www.thenewstep.cn/backend/7001'; 
  1. 打包项⽬

    1. npm run build 
    • 得到build文件夹后,将里面所有静态文件放到服务器中的/frontend/react/7001文件夹中
  2. 进入宝塔,创建项⽬多级⽬录

    • 宝塔左侧菜单 "⽂件" -> 根⽬录/www/wwwroot/www.thenewstep.cn/frontend
    • 在frontend⽂件夹下创建react
    • 在react⽂件夹下创建7001
    • 将打包好的静态⽂件,放⼊7001⽂件夹中
  3. 浏览器运⾏测试

​ 访问:https://www.thenewstep.cn/frontend/react/7001

  1. 手动刷新报错问题

1)产生问题的原因

React单页应用在使用React-Router后,在本地环境中测试一切正常,但在发布到基于Nginx的生产环境后出现了刷新后返回404 Not Found错误。

该问题产生的原因为加载单页应用后路由改变均由浏览器处理,而刷新时将会请求当前的链接,而Nginx无法找到对应的页面。例如打开页面http://example.com后跳转至http://example.com/page1,实际上只是由浏览器根据URL解析后加载对应的组件并渲染,而不再向服务器请求对应的页面。

当刷新时,浏览器将请求http://example.com/page1页面,此时由于资源中并不存在该页面,便导致了返回404的情况。

2)解决方法一

  • 需要配置nginx代理服务的try_files命令,该命令用于根据指定的参数依次检查寻找对应的文件,若所有文件都找不到将会在内部重定向至最后一个参数指定的文件。
  1. location /frontend/react/7001 { 
  2. try_files $uri $uri/ /frontend/react/7001; 
  3. index index.html; 
  4. } 
  • $uri代表请求的文件及其路径,$uri/表示对应路径的目录。例如请求http://example.com/page时,$uri表示资源目录下是否存在名为page的文件,$uri/表示名为page的目录。

    所以,我们在配置文件中增加的命令表示接收到请求时先寻找uri对应的文件或目录,若不存在则返回index.html文件。

3)解决方法二:

​ 可以尝试 将BrowserRouter修改为HashRouter。

posted @ 2023-03-13 11:07  sxl7777  阅读(100)  评论(0编辑  收藏  举报