全栈开发后台资金管理系统项目


title: 全栈开发后台资金管理系统项目date: 2019-11-27 15:58:28

tags: 项目

后端部分

1. nodemon 使用

当我们做服务器的时候,每次变更都要重启才能生效。

如:我们创建了一个名为 server.js 的文件,作为服务器

使用node ./server.js即可启动,但我们对其修改后,要看效果就要关闭之前的再启动。

而 nodemon 帮我们解决这个问题。

1
npm install nodemon -g //全局安装nodemon

然后就可以使用 nodemon 运行我们的服务器了

1
nodemon ./server.js

这时,修改文件,服务器会自动重启。

将命令设置到 package.json。

1
2
3
4
5
//在package.json中修改
"scripts": {
"start": "node server.js",
"server": "nodemon server.js"
},

这样在就可以使用npm run startnpm run server来运行服务器

2.连接数据库

1
2
//node-app下执行
npm install mongoose

为方便修改配置,新建文件 /config/keys.js, 内容:

1
2
3
4
module.exports = {
mongoURI:
"mongodb://<username>:<password>@cluster0-shard-00-00-oqdfe.mongodb.net:27017,cluster0-shard-00-01-oqdfe.mongodb.net:27017,cluster0-shard-00-02-oqdfe.mongodb.net:27017/test?ssl=true&replicaSet=Cluster0-shard-0&authSource=admin&retryWrites=true&w=majority"
};

该链接需要到 mongoDB 官网注册账户,获取 500M 免费空间,创建一个 Preject 再创建 Clusters,之后点击”Connect”,选择”Connect your Application”进入下一步,域名选择默认确定即可.DRIVER 选择“node.js”, VERSION 选择”2.2.12 or later”,然后 copy 下面的链接即可,注意修改:为对应的用户名和密码。可以在”Database Access”中添加和修改用户。

在 server.js 中引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//DB config
const db = require("./config/keys").mongoURI;

//连接数据库
//Connect to mongodb
mongoose.connect(db, {
//第一个参数db是在线数据库的地址,也可以直接将地址写入这里,美观起见,另写一个文件存储
useNewUrlParser: true,//防止弃用警告
useUnifiedTopology: true,//防止弃用警告
useFindAndModify: false //防止弃用警告
})
//提供promise调用
.then(() => console.log("mongoDB Connected")) //成功
.catch(err => console.log(err)); //失败

3.配置路由和接口

在 node-app 下创建 /route/api/users.js,内容:

用于登录和注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//users.js

//引入express
const express = require("express");
//实例化路由
const router = express.Router();

// $route GET api/users/test
//@desc 返回请求的json数据
//@access public(公有接口)

//验证路由,访问/test,将返回`msg:"login works"`
router.get("/test", (req, res) => {
//返回json数据
res.json({ msg: "login works" });
});

//导出router
module.exports = router;

在 server.js 中引用和使用

1
2
3
4
5
6
7
8
//server.js

const express = require('express')
const app = express()
//引入users.js
const users = require("./route/api/users");
//路由访问这个地址时,就会访问users
app.use("/api/users", users);

这时 使用浏览器访问 http://localhost:5000/api/users/test即可看到返回的msg:"login works"

4.创建模型

新建 /models/User.js,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//User.js

//引入mongoose.可以将数据存储到mongoose
const mongoose = require("mongoose");
//创建Schema模型
const Schema = mongoose.Schema;

//create Schema
const UserSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String
},
avatar: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});

module.exports = User = mongoose.model("users", UserSchema);

4.5 下载postman,安装

可以用来测试接口是否通.

4.6 创建register接口

首先需要安装body-parser.

5.配置注册

安装 body-parser,方便发送 POST 请求

1
npm install body-parser

在 server.js 中引用

1
2
3
4
5
const bodyParser = require("body-parser");

//使用body-parser中间件
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

在 users.js 中配置接口

1
2
3
4
5
6
//$route POST api/uers/register
//@desc 返回请求的JSON数据
//@access public (公有接口)
router.post("/register", (req, res) => {
console.log(req.body); //用来测试是否连接
});

//此处如果连接不上mongoDB,可能是白名单失效.再添加一个白名单在mongoDB即可.

功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.是否有邮箱

//
router.post("/register", (req, res) => {
//查询数据库中是否拥有邮箱
User.findOne({ email: req.body.email }).then(user => {
if (user) {
//如果存在
return res.status(400).json({ email: "邮箱已被注册!" });
} else {
//否则不存在
const newUser = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
}
});
});
密码加密

安装 bcrypt

1
npm install bcrypt

在 users.js 中引入,

1
const bcrypt = require("bcrypt");

官方详细说明链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bcrypt.genSalt(10, function(err, salt) {
//10是加密的一种模式
bcrypt.hash(newUser.password, salt, (err, hash) => {
//newUser.password 是加密对象,salt是回调函数,最后是加密结果
if (err) throw err; //如果存在错误,则抛出。 throw是js语法,抛出

//没有错误,则把加密过的密码hash赋值给password
newUser.password = hash;
//将newUser存储
newUser
.save()
.then(user => res.json(user))
.catch(err => console.log(err));
});
});

头像 avatar

gravatar 官方说明链接

安装

1
npm i gravatar

user.js 中引入

1
const gravatar = require("gravatar");

在接口位置使用(user.js)

1
2
3
4
5
6
7
8
9
10
11
if (user) {
return res.status(400).json({ email: "邮箱已被注册!" });
} else {
const avatar = gravatar.url(req.body.email, { s: "200", r: "pg", d: "mm" }); //s是大小。r是头像格式。mm是灰色的头像
const newUser = new User({
name: req.body.name,
email: req.body.email,
avatar, //引入头像
password: req.body.password
});
}

如何得到头像?

  1. 打开 gravatar 网址
  2. 注册 gravatar,其注册实际是注册了 wordpress.com 网站的账户,然后登录 gravatar,任意格式的邮箱均可申请成功,但无法收到邮件,则无法验证并修改头像。因此要使用可以收到验证的邮箱。
  3. 上传头像。上传图片时,最后会选择图片会有Choose a rating for your Gravatar ,有四个选项,G、PG、R、X,这里我们选择 pg,我们在使用时也是r: 'pg',需要保持一致。

这时,我们使用 postman 向 http://localhost:5000/api/users/register 发送 post 请求,使用(application/x-www-form-urlencoded)(key:email value:user@usertest.com) 就能得到设置的头像了。

7.登录接口

users.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//$route POST api/users/login
//@desc 返回taken jwt passport
//@access public (公有接口)
router.post("/login", (req, res) => {
const email = req.body.email;
const password = req.body.password;

//查询数据库,看email是否存在
User.findOne({ email }).then(user => {
if (!user) {
return res.status(404).json({ email: "用户不存在" }); //如果用户不存在
}
//如果email存在
//密码匹配
//第一个password是前端传入密码,user.password是系统内密码
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
res.json({ msg: "success" }); //如果密码对比正确,(实际这里返回token,但暂时先返回msg)
} else {
return res.status(400).json({ password: "密码错误!" }); //如果密码对比不正确
}
});
});
});

返回 token

安装 jsonwebtoken (jwt)

1
npm install jsonwebtoken

在 users.js 引入

1
const jwt = require("jsonwebtoken");

在密码验证成功处插入

我们在 config/keys.js 导出的对象中,加入了 secretOrKey:”secret” 属性和值,再引入到 users.js 以方便统一管理配置。

过期时间的 3600 单位为秒

token 前必须是 “Bearer ”(送信人的意思),末尾空格也不可缺少。

如果 success 为 true,就应该得到 token 值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//替换上面的res.json({ msg: "success" });

//jwt.sign("规则","加密名字","过期时间","箭头函数")

const rule = { id: user.id, name: user.name }; //可以更多
//sign签名
jwt.sign(rule, keys.secretOrKey, { expiresIn: 3600 }, (err, token) => {
if (err) throw err;
res.json({
success: true,
//"Bearer "前缀是固定的,意思是送信者.后面有个空格
token: "Bearer " + token
});
});
// res.json({msg:"success"});

验证 token

token相当于一个令牌或者钥匙.

使用passport-jwt进行token验证.

users.js 加入接口

1
2
3
4
5
6
7
8
9
//$route GET api/users/current
//@desc return current user
//@access Privates

//router.get("/current", "验证token",(req, res) => {
//在中间验证token,但是需要passport,还没装,会报错,暂时删掉
router.get("/current", (req, res) => {
res.json({ msg: "success" }); //测试使用,后期有修改
});
安装 passport-jwt 和 passport
1
npm install passport-jwt passport

passport 网址

passport-jwt 网址

server.js 中引入,并初始化

1
2
3
4
5
//server.js

const passport = require("passport");
//passport初始化
app.use(passport.initialize());

新建文件 /config/passport.js,内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const JwtStrategy = require("passport-jwt").Strategy,
ExtractJwt = require("passport-jwt").ExtractJwt;
const mongoose = require("mongoose");
const User = mongoose.model("users");
const keys = require("../config/keys");

const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = keys.secretOrKey;

module.exports = passport => {
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
//console.log(jwt_payload);
//通过id获取用户
User.findById(jwt_payload.id)
//获取成功
.then(user => {
//如果用户存在
if (user) {
return done(null, user);
}
//如果用户不存在
return done(null, false);
})
//获取失败
.catch(err => console.log(err));
})
);
};

在 server.js 中引入 passport.js

1
2
3
4
//server.js

app.use(passport.initialize());
require("./config/passport")(passport); //这样代码就不需要在当前server.js中写了

这里使用了一个技巧,require("xxx.js")(对象) 将对象传入xxx.js,同时将该js引入当前文件中。这样就可以在xxx.js中编写代码,实现分离,而且在xxx.js可以使用传入的对象。

在 users.js 中引入 passport,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//users.js

const passport = require("passport");

//完成token验证,返回部分信息

//$route GET api/users/current
//@desc return current user
//@access Privates
router.get(
"/current",
passport.authenticate("jwt", { session: false }),
(req, res) => {
res.json({
id: req.user.id,
name: req.user.name,
email: req.user.email
});
}
);

这里调整了一些输出的内容,将输出对象改为了字符串,可能造成代码实际和上面有些出入。


添加身份

如果想在 user 中添加其他信息(比如添加管理员)可参考此内容

models/User.js 的 UserSchema 中添加身份字段

1
2
3
4
identity:{
type:String,
required:true
},

api/users.js 中加入信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// newUser中
const newUser = new User({
name: req.body.name,
email: req.body.email,
avatar,
password: req.body.password,
identity: req.body.identity, //添加的信息
});

//密码匹配规则中
const rule = {
id: user.id,
name: user.name,
avatar: user.avatar,
identity: user.identity,
};

//验证token输出信息时
router.get(
"/current",
passport.authenticate("jwt", { session: false }),
(req, res) => {
res.json({
id: req.user.id,
name: req.user.name,
email: req.user.email,
identity: req.user.identity, //添加的内容
});
}
);

配置信息接口

新建 models/Profile.js 建立 ProfileSchema,内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

//create Schema
const ProfileSchema = new Schema({
type: {
type: String,
},
describe: {
type: String,
},
income: {
type: String,
required: true,
},
expend: {
type: String,
required: true,
},
cash: {
type: String,
required: true,
},
remark: {
type: String,
},
date: {
type: Date,
default: Date.now,
},
});

module.exports = Profile = mongoose.model("profile", ProfileSchema);

新建 api/profiles.js 暂不写内容,将其在 server.js 中引入

1
2
3
4
5
//server.js
const profiles = require("./route/api/profiles");

//使用route
app.use("/api/profiles", profiles);

在 api/profiles.js 配置信息进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//@login & register
const express = require("express");
const router = express.Router();
const passport = require("passport");
const Profile = require("../../models/Profile");

//$route GET api/profile/test
//@desc 返回请求的JSON数据
//@access public (公有接口)
router.get("/test", (req, res) => {
res.json({ msg: "Profile works" });
});

module.exports = router;

postman 发送到 http://localhost:5000/api/profiles/test 返回 Profile works 即链接成功。

更改数据库接口

如果要更改数据库接口,可以/config/keys.js中的mongoURI的值,该值的获取方法,参考上述创建时的内容。

创建添加信息的接口

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//$route POST api/profile/add
//@desc 创建信息接口
//@access Private
router.post(
"/add",
passport.authenticate("jwt", { session: false }),
(req, res) => {
const profileFields = {};
if (req.body.type) profileFields.type = req.body.type;
if (req.body.describe) profileFields.describe = req.body.describe;
if (req.body.income) profileFields.income = req.body.income;
if (req.body.expend) profileFields.expend = req.body.expend;
if (req.body.cash) profileFields.cash = req.body.cash;
if (req.body.remark) profileFields.remark = req.body.remark;
new Profile(profileFields).save().then((profile) => {
res.json(profile);
});
}
);

获取所有信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//$route GET api/profile
//@desc 获取所有信息
//@access Private

router.get(
"/",
passport.authenticate("jwt", { session: false }),
(req, res) => {
Profile.find()
.then((profile) => {
if (!profile) {
return res.status(404).json("没有任何内容");
}
res.json(profile);
})
.catch((err) => res.status(404).json("err"));
}
);

获取单个信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//$route GET api/profile/:id
//@desc 获取单个信息
//@access Private

router.get(
"/:id",
passport.authenticate("jwt", {
session: false,
}),
(req, res) => {
Profile.findOne({ _id: req.params.id })
.then((profile) => {
if (!profile) {
return res.status(404).json("没有任何内容");
}
res.json(profile);
})
.catch((err) => res.status(404).json(err));
}
);

编辑信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//$route POST api/profile/edit
//@desc 编辑信息接口
//@access Private
router.post(
"/edit/:id",
passport.authenticate("jwt", {
session: false,
}),
(req, res) => {
const profileFields = {};
if (req.body.type) profileFields.type = req.body.type;
if (req.body.describe) profileFields.describe = req.body.describe;
if (req.body.income) profileFields.income = req.body.income;
if (req.body.expend) profileFields.expend = req.body.expend;
if (req.body.cash) profileFields.cash = req.body.cash;
if (req.body.remark) profileFields.remark = req.body.remark;
Profile.findOneAndUpdate(
{ _id: req.params.id },
{ $set: profileFields },
{ new: true }
).then((profile) => res.json(profile));
}
);

删除信息

profiles.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//$route delete api/profile/delete/:id
//@desc 删除信息接口
//@access Private
router.delete(
"/delete/:id",
passport.authenticate("jwt", {
session: false,
}),
(req, res) => {
Profile.findOneAndRemove({ _id: req.params.id })
.then((profile) => {
profile.save().then((profile) => res.json(profile));
})
.catch((err) => res.status(404).json("删除失败"));
}
);

至此,信息的增删改查均已实现。要创建其他 schema 可以参考此方式

前后端连载

查看 vue 版本,是否在 3.0.0 以上,我们要求是在 3.0.0 以上。

vue-cli 的安装见 vue 官网 ,这里就不说了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//终端中执行
vue -V //查看vue-cli版本 本案例要求3.0.0以上

//创建项目 client是自己起的名字,意为"客户端"
vue create client

接下来进入选择流程,后面 √ 为我们作出的选择项,-----表示回车到下一选项页
? Please pick a preset:
default (babel, eslint) (默认配置)
Manually select features (手动选择) √
-----
按键盘a表示全选,i表示反选,空格键 表示切换选中,如果你需要什么就选什么就可以了,这里选择BabelRouterVuex
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ Babel
TypeScript
Progressive Web App (PWA) Support
Router
Vuex
CSS Pre-processors
Linter / Formatter
Unit Testing
E2E Testing
-----
是否使用history ,我们输入y,回车,会继续显示其他问题。
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n)

In dedicated config files
In package.json

Save this as a preset for future projects? (y/N) (是否要保存你当前预制模板) N (第一次时可以保存一次方便之后用)
-----
接下来就是等待安装成功。 会产生一个 client的文件夹

//启动项目
cd client
npm run serve //注意,是serve 不是server

此时使用 http://localhost:8080/ 就可以打开前端了,再新建终端,执行 nodemon就打开了后台。

这需要两个终端打开,较为繁琐,因此采用前后端连载,借助 concurrently 将多个终端启动的项目绑在一起

安装 concurrently

1
npm install concurrently

打开 /client/package.json

1
2
3
4
5
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"start": "npm run serve"
},

此时,我们在 client 中 使用 npm run start即可启动前端

在根目录的 package.json 中配置client-stallclientdev

1
2
3
4
5
6
7
"scripts": {
"client-install":"npm install --prefix client",
"client":"npm start --prefix client",
"start": "node server.js",
"server": "nodemon server.js",
"dev":"concurrently \"npm run server\" \"npm run client\""
},

此时我们可以在根目录的终端下执行npm run dev即可同时启动 前端和后台

前端部分

接后端部分,此文档为 前端部分 内容。

准备工作

为使内容整洁,我们将 vue-cli 创建项目时生成的我们不需要的文件进行整理

我们接下来更多的是在 client 这个文件夹下工作,非强调指明,则以 client 视为根目录。

  • 删除 /src/assets/logo.png  (vue 的 logo 图片)
  • 删除 /src/components/HelloWorld.vue
  • 删除 /src/views/ 中的 About.vue 和 Home.vue
  • 新建 /src/views/Index.vue,内容为
1
2
3
4
5
6
7
8
9
10
<template>
<div class="index">初始化页面</div>
</template>

<script>
export default {
name: "index",
components: {},
};
</script>
  • 打开 /src/router.js,重新整理为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from "vue";
import Router from "vue-router";
import Index from "./views/Index.vue";

Vue.use(Router);

export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
redirect: "/index",
},
{
path: "/index",
name: "index",
component: Index,
},
],
});
  • 打开 /src/App.vue,重新整理为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div id="app">
<router-view />
</div>
</template>

<style>
html,
body,
#app {
width: 100%;
height: 100%;
}
</style>
  • 新建 /public/css/reset.css ,在 /public/index.html 中引入该 css 文件
1
<link rel="stylesheet" href="css/reset.css" />

reset.css 内容可以访问 CSS reset得到,也可在下面设置自己需要的初始样式

本案例中,我们在 reset.css 中追加了 el 中加载相关的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.el-loading {
position: absolute;
z-index: 2000;
background-color: rgba(255, 255, 255, 0.7);
margin: 0;
padding: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
}
.el-loading-spinner {
top: 50%;
margin-top: -21px;
width: 100%;
text-align: center;
position: absolute;
}

注册页和 404

安装 elementUI

1
2
//此时目录在client中
npm i element-ui -S

在/src/main.js 中引入

1
2
3
4
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";

Vue.use(ElementUI);

将 图片文件 放在 /src/asset/ 文件夹下面

分别是 404.gif,bg.jpg,logo.png,showcase.png

初始注册,新建 /src/views/Register.vue,内容 (只是简单布局,还未设置表单内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div class="register">
<section class="form_container">
<div class="manage_tip">
<span class="title">万事屋在线后台管理系统</span>

</div>
</section>
</div>
</template>

<script>
export default {
name:"register",
components:{}
};
</script>

<style scoped>
.register{
position: relative;
width: 100%;
height: 100%;
background: url(../assets/bg.jpg) no-repeat center center;
background-size: 100% 100%;
}
.form_container{
width: 370px;
height: 210px;
position: absolute;
top:10%;
left:34%;
padding:25px;
border-radius: 5px;
text-align: center;
}
.form_container .manage_tip .title{
font-family: 'Microsooft YaHei';
font-weight: bold;
font-size: 26px;
color: #fff;
}
</style>

在 router.js 中设置路由

1
2
3
4
5
6
7
8
9
//引入组件
import Register from './views/Register.vue'

//添加路由
{
path: '/register',
name:'register',
component: Register
},

设置 404 页面

  1. 新建 /src/views/404.vue 组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="notfound">
<img src="../assets/404.png" alt="页面没找到">
</div>
</template>

<style scoped>
.notfound{
width: 100%;
height:100%;
overflow: hidden;
}
.notfound img{
width: 100%;
height:100%;
}
</style>

在 router.js 中设置路由

1
2
3
4
5
6
//router.js
{
path: '*',
name:'/404',
component: NotFound
}

注册表单

此后大量使用 element 的代码,为避免过长,仅提一些重要的点,其他请结合原文件阅读笔记

密码规则与验证

加载动画和消息提示

安装 axios

1
2
//client目录下
npm install axios

新建 /src/http.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import axios from "axios";
import { Message, Loading } from "element-ui";

let loading;
function startLoading() {
loading = Loading.service({
lock: true,
text: "拼命加载中...",
background: "rgba(0,0,0,0.7)",
});
}

function endLoading() {
loading.close();
}

//请求拦截
axios.interceptors.request.use(
(config) => {
//加载动画
startLoading();
return config;
},
(error) => {
return Promise.reject(error);
}
);

//响应拦截

axios.interceptors.response.use(
(response) => {
//结束加载动画
endLoading();
return response;
},
(error) => {
//错误提醒
endLoading();
Message.error(error.response.data);
return Promise.reject(error);
}
);

export default axios;

在 main.js 中引用

1
2
3
import axios from "./http";

Vue.prototype.$axios = axios;

http.js 内容(后会变动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import axios from "axios";
import { Message, Loading } from "element-ui";

let loading;
function startLoading() {
loading = Loading.service({
lock: true,
text: "拼命加载中...",
background: "rgba(0,0,0,0.7)",
});
}

function endLoading() {
loading.close();
}

//请求拦截
axios.interceptors.request.use(
(config) => {
//加载动画
startLoading();
return config;
},
(error) => {
return Promise.reject(error);
}
);

//响应拦截

axios.interceptors.response.use(
(response) => {
//结束加载动画
endLoading();
return response;
},
(error) => {
//错误提醒
endLoading();
Message.error(error.response.data);
return Promise.reject(error);
}
);

export default axios;

配置前端跨域请求(使用 vue-cli 项目)

新建 vue.config.js ,在 client 目录下,内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const path = require("path");
const debug = process.env.NODE_ENV !== "production";

module.exports = {
baseUrl: "/", //根域上下文目录
outputDir: "dist", //构建输出目录
assetsDir: "assets", //静态资源目录(js,css,img,fonts)
lintOnSave: false, //是否开启eslint保存检测,有效值:true|false|'error'
runtimeCompiler: true, //运行时版本是否需要编译
transpileDependencies: [], //默认babel-loader忽略node_modules,这里可增加例外的依赖包名
productionSourceMap: true, //是否在构建生产包时生产 sourceMap 文件,false将提高构建速度
configureWebpack: (config) => {
//webpack 配置,值为对象时会合并配置,为方法时会改写配置
if (debug) {
//开发环境配置
config.devtool = "cheap-module-eval-source-map";
} else {
//生产环境配置
}
// Object.assign(config,{ //开发生产共同配置
// resolve:{
// alias:{
// '@':path.resolve(__dirname,'./src'),
// '@c':path.resolve(__dirname,'./src/components'),
// 'vue$':'vue/dist/vue.esm.js'
// }
// }
// })
},
chainWebpack: (config) => {
//webpack链接API,用于生成和修改webpack配置,
// https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
if (debug) {
//本地开发配置
} else {
//生产开发配置
}
},
parallel: require("os").cpus().length > 1, //构建时开启多进程处理babel编译
pluginOptions: {
//第三方插件配置
},
pwa: {
//单页插件相关配置
// https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
},
devServer: {
open: true,
host: "localhost",
port: 8080,
https: false,
hotOnly: false,
proxy: {
//配置跨域
"/api": {
target: "http://localhost:5000/api",
ws: true,
changOrigin: true,
pathRewrite: {
"^/api": "",
},
},
},
before: (app) => {},
},
};

这样就可以访问我们的后台了

在 register.vue 中配置跳转,这样就可以注册用户了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
submitForm(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
this.$axios
.post("/api/users/register", this.registerUser)
.then(res => {
//注册成功
this.$message({
message: "账号注册成功!",
type: "success"
});
});
this.$router.push("/login");
}
});
}

登录逻辑

新建组件和添加路由参考注册,这里讲下登录逻辑. Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
submitForm(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
this.$axios.post("/api/users/login", this.loginUser).then(res => {
//拿到token
const { token } = res.data;
// 存储到localStorage
localStorage.setItem("eleToken", token);
this.$router.push('/index');
});
this.$router.push("/login");
}
});
}

路由守卫

router.js

1
2
3
4
5
6
7
8
9
10
11
//路由守卫
router.beforeEach((to, from, next) => {
//如果token存在返回boolean值true,否则false
const isLogin = localStorage.eleToken ? true : false;
if (to.path == "/login" || to.path == "/register") {
next();
} else {
//如果有token,为true,就正常跳转;为false,就跳转到登录页
isLogin ? next() : next("login");
}
});

设置 token 和 token 过期

在请求拦截中,如果存在 token,就把 token 设置到请求头中.

在响应拦截里的 error 里,如果状态码是 401 未授权,表示 token 过期.就在 error 返回函数里清除 token,并跳转到登录页.

http.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//请求拦截
axios.interceptors.request.use(
(config) => {
//加载动画
startLoading();
//如果有token
if (localStorage.eleToken) {
//设置统一的请求头header
config.headers.Authorization = localStorage.eleToken;
}

return config;
},
(error) => {
return Promise.reject(error);
}
);

//响应拦截
axios.interceptors.response.use(
(response) => {
//结束加载动画
endLoading();
return response;
},
(error) => {
//错误提醒
endLoading();
Message.error(error.response.data);

//获取错误状态码
const { status } = error.response;
//401未授权,表示token过期
if (status == 401) {
Message.error("token失效,请重新登录!");
//清除token
localStorage.removeItem("eleToken");
//跳转到登录页面
router.push("/login");
}
return Promise.reject(error);
}
);

解析 token 存储到 Vuex 中

安装解析 token 的模块

1
npm install jwt-decode
1
2
3
4
5
6
7
8
9
10
11
//Login.vue

//引入解析模块
import jwt_decode from "jwt_decode";

//解析token
const decoded = jwt_decode(token);

//token存储到vuex中
this.$store.dispatch("setAuthenticated", !this.isEmpty(decoded));
this.$store.dispatch("setUser", decoded);

设置 Vuex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//store.js

//登录成功后将数据存储到Vuex中
//判断是否认证通过
const types = {
SET_AUTHENTICATED: "SET_AUTHENTICATED",
SET_USER: "SET_USER",
};

const state = {
isAuthenticated: false,
user: {},
};

const getters = {
isAuthenticated: (state) => state.isAuthenticated,
user: (state) => state.user,
};

const mutations = {
[types.SET_AUTHENTICATED](state, isAuthenticated) {
if (isAuthenticated) state.isAuthenticated = isAuthenticated;
else state.isAuthenticated = false;
},

[types.SET_USER](state, user) {
if (user) state.user = user;
else state.user = {};
},
};

const actions = {
setAuthenticated: ({ commit }, isAuthenticated) => {
commit(types.SET_AUTHENTICATED, isAuthenticated);
},
setUser: ({ commit }, user) => {
commit(types.SET_USER, user);
},
//清除当前的状态
clearCurrentState: ({ commit }) => {
commit(types.SET_AUTHENTICATED, false);
commit(types.SET_USER, null);
},
};

export default new Vuex.Store({
state,
getters,
mutations,
actions,
});

方法: 判断是否为空

1
2
3
4
5
6
7
isEmpty(value){
return (
value === undefined ||
value === null ||
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0)
)

在根组件 App.vue 中判断 token

1
2
3
4
5
6
7
8
9
created() {
if (localStorage.eleToken) {
//解析token
const decoded = jwt_decode(localStorage.eleToken);
//token存储到vuex中
this.$store.dispatch("setAuthenticated", !this.isEmpty(decoded));
this.$store.dispatch("setUser", decoded);
}
}

样式

新建/component/HeadNav.vue

将 HeadNav.vue 引入到 Index.vue,并注册,然后 template 中调用

在 HeadNav.vue 中布局

1
//看代码

写向下箭头的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  methods: {
setDialogInfo(cmdItem) {
switch (cmdItem) {
case "info":
this.showInfoList();
break;
case "logout":
this.logout();
break;
}
},
showInfoList() {
console.log("个人信息");
},
logout() {
//清除token
localStorage.removeItem("eleToken");
//设置Vuex store
this.$store.dispatch("clearCurrentState");
//跳转到login
this.$router.push("/login");
}
}
};
//vuex中记得在actions中添加clearCurrentState

个人信息

新建 views/Home.vue

在 router.js 中设置二级路由

新建 views/InfoShow.vue

侧面导航栏

新建 assets/component/LeftMenu.vue

编辑收支类型

创建资金列表

新建 views/FundList.vue

添加各个按钮,事件

设置添加按钮,新增对话框 component/Dialog.vue 组件

编辑和添加

编辑和添加功能雷同,把 formData 放到父级 fundlist 中,并用 props 传递

修改父级中 dialog 的属性,新增 title,options,方便切换弹窗的标题和选项

在 handleEdit 中修改 dialog 的 title,当点击编辑时 title 就切换成’编辑’

编辑时已经拿到数据了.this.formData 的值也就是传入值了

同样,添加的也可以新增 this.formData.但是添加的数据默认是空的

在 onSubmit 中判断提交的类型

1
const url = this.dialog.option == "add" ? "add" : `edit/${this.formData.id}`;

删除按钮

1
2
this.$axios.delete(`/api/profiles/delete/${row._id}`);
//之后可以then调用$message弹出删除成功的提示

分页

elementUI 布局整行分为 24 列.

使用标准分页

1
2
3
4
5
6
7
8
9
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage4"
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
layout="total, sizes, prev, pager, next, jumper"
:total="400">
</el-pagination>

//修改绑定数据

//设置全部数据容器(数组)

allTableData: []

//在获取数据时就开始设置分页数据

this.setPaginations()

在 setPaginations 中设置默认属性

筛选和权限

定义筛选组件

复制 elementUI 的时间选择器.

添加筛选按钮,绑定筛选事件 handleSearch()

绑定开始时间,结束时间.在 data 中定义开始时间 startTime,结束时间 endTime

添加过滤容器 filterTableData:{},在 getProfile()时也存储一次.

权限

使用计算属性 computed 获取此时用户的身份

1
2
3
4
computed: {
user(){
return this.$store.getters.user
}

使用 v-if 决定是否可以使用添加,编辑,删除操作.

1
2
v-if="user.indentity == 'manager'"
//将此判断加到添加事件之前和label='操作'后