一、Node+Vue实战项目

1.1 创建Node项目、Vue项目

mkdir classweb
cd classweb/express servervue init webpack vueclient
.
|-- server
|   |-- app.js
|   |-- bin
|   |-- node_modules
|   |-- package-lock.json
|   |-- package.json
|   |-- public
|   |-- routes
|   `-- views
|-- tree.txt
`-- vueclient|-- README.md|-- build|-- config|-- index.html|-- node_modules|-- package.json|-- src`-- static12 directories, 7 files

1.2 安装mongodb操作软件 Robomongo

create database 输入创建 classweb数据库
展开classweb,然后在collections右键, create collection 创建一个user表
user右键 insert document,然后输入后面的数据 ,save, (数据用户名 admin 密码是 123456 加密后的字段 还有手机号)

    "name" : "admin","phone" : "13388868886","password" : "4QrcOUm6Wau+VuBX8g+IPg=="








1.3 实现登录功能

// App.vue<template><div id="app"><router-view/></div>
</template><script>
export default {name: 'App'
}
</script>

componets文件夹中新建 login.vue

// Login.vue<template><div class="backlogin"><div class="login_box"><div class="title">后台登录</div><div><input type="text" placeholder="手机号/用户名" v-model="username" class="myinput" /></div><div><input type="password" placeholder="口令" v-model="password" class="myinput" /></div><div class="login_other"><a href="javascript:;">找回密码</a><input type="checkbox" id="rememberme" /><label for="rememberme">记住我</label></div><button :disabled="disablebtn" class="login">登录</button></div></div>
</template><script>
export default {name: 'HelloWorld',data () {return {username: "admin", /* 先预存测试值,以免手动输入 */password: "123456",disablebtn: false}}
}
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>.login_box {width: 320px;margin: 50px auto;}.login_box .title {color: #273444;font-size: 1.5em;text-align: center;margin: 0 0 20px 0;}.login_box .myinput {width: 100%;border: 1px solid #cad3d3;height: 40px;line-height: 40px;margin: 5px 0 10px;border-radius: 3px;padding: 0 10px;outline: none;box-sizing: border-box;}.login_box .myinput:focus {border: 1px solid #4289dc;}.login_other {overflow: hidden;}.login_other a {float: right;color: #727f8f;}.login_other a:hover {color: #273444;}.login_other input, .login_other label {float: left;color: #727f8f;}.login_other input {margin: 4px 5px 0 0;}.login {box-sizing: border-box;border: 0;height: 44px;line-height: 44px;width: 100%;background: #4187db;font-size: 16px;border-radius: 3px;margin-right: 40px;transition: all 0.5s ease;cursor: pointer;outline: none;color: #fff;margin-top: 15px;}.login:hover {background: #2668b5;}.login[disabled] {opacity: .8;}.login[disabled]:hover {background: #4187db;}@media only screen and (max-width: 768px) {.login_box {width: 280px;margin: 50px auto;}}
</style>

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'Vue.use(Router)export default new Router({routes: [{path: '/',name: 'Login',component: Login}]
})

登录功能实现

前端功能实现

先安装axios
npm i axios –save

// main.js// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'Vue.config.productionTip = false// 引入axios,并配置基础路径
// 又因是跨域请求node端,所以所有请求前页面都要添加node端的基础地址,以后打包上线时再删掉
// 又因是跨域请求,需要配置withCredentials为true,这样避免每次都被识别为新的请求
// 说明:在vue中,可以使用代理去实现跨域,但是每次新地址都需要配置,还是比较麻烦,这里我们采用直接配置跨域,一次配置就可以一劳永逸
import axios from 'axios'
axios.defaults.withCredentials = true // 跨域保存session
axios.defaults.baseURL = "http://localhost:3000" // 默认基础路径配置,打包时删掉
Vue.prototype.$axios = axios/* eslint-disable no-new */
new Vue({el: '#app',router,components: { App },template: '<App/>'
})

在Login.vue中写登录的具体方法

// Login.vue<template><div class="backlogin"><div class="login_box"><div class="title">后台登录</div><div><input type="text" placeholder="手机号/用户名" v-model="username" class="myinput" /></div><div><input type="password" placeholder="口令" v-model="password" class="myinput" /></div><div class="login_other"><a href="javascript:;">找回密码</a><input type="checkbox" id="rememberme" /><label for="rememberme">记住我</label></div><button :disabled="disablebtn" @click="login" class="login">{{ loginText }}</button></div></div>
</template><script>
export default {name: 'HelloWorld',data () {return {username: "admin", /* 先预存测试值,以免手动输入 */password: "123456",disablebtn: false,loginText: "登录"}},methods: {login () {this.disablebtn = truethis.loginText = "登录中..."this.$axios.post('/users/login', {username: this.username,password: this.password}).then((result) => {// 成功console.log(result);this.disablebtn = falsethis.loginText = "登录"}).catch((error) => {// 失败this.disablebtn = falsethis.loginText = "登录"})}}
}
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>.login_box {width: 320px;margin: 50px auto;}.login_box .title {color: #273444;font-size: 1.5em;text-align: center;margin: 0 0 20px 0;}.login_box .myinput {width: 100%;border: 1px solid #cad3d3;height: 40px;line-height: 40px;margin: 5px 0 10px;border-radius: 3px;padding: 0 10px;outline: none;box-sizing: border-box;}.login_box .myinput:focus {border: 1px solid #4289dc;}.login_other {overflow: hidden;}.login_other a {float: right;color: #727f8f;}.login_other a:hover {color: #273444;}.login_other input, .login_other label {float: left;color: #727f8f;}.login_other input {margin: 4px 5px 0 0;}.login {box-sizing: border-box;border: 0;height: 44px;line-height: 44px;width: 100%;background: #4187db;font-size: 16px;border-radius: 3px;margin-right: 40px;transition: all 0.5s ease;cursor: pointer;outline: none;color: #fff;margin-top: 15px;}.login:hover {background: #2668b5;}.login[disabled] {opacity: .8;}.login[disabled]:hover {background: #4187db;}@media only screen and (max-width: 768px) {.login_box {width: 280px;margin: 50px auto;}}
</style>

后台功能实现

routes中创建dbhandler.js文件,写入下面我们封装好的mongodb操作方法

// dbhandler.jsvar mongo=require("mongodb");
var MongoClient = mongo.MongoClient;
var assert = require('assert');
var url = require('url');
var host="localhost";
var port="27017";
var Urls = 'mongodb://localhost:27017/classweb';
// classweb  ===> 自动创建一个//add一条数据
var add = function(db,collections,selector,fn){var collection = db.collection(collections);collection.insertMany([selector],function(err,result){try{assert.equal(err,null)}catch(e){console.log(e);result = [];};fn(result);db.close();});
}
//delete
var deletes = function(db,collections,selector,fn){var collection = db.collection(collections);collection.deleteOne(selector,function(err,result){try{assert.equal(err,null);assert.notStrictEqual(0,result.result.n);}catch(e){console.log(e);result.result = "";};fn( result.result ? [result.result] : []); //如果没报错且返回数据不是0,那么表示操作成功。db.close;});
};
//find
var find = function(db,collections,selector,fn){//collections="hashtable";var collection = db.collection(collections);collection.find(selector).toArray(function(err,result){//console.log(docs);try{assert.equal(err,null);}catch(e){console.log(e);result = [];}fn(result);db.close();});}//update
var updates = function(db,collections,selector,fn){var collection = db.collection(collections);collection.updateOne(selector[0],selector[1],function(err,result){try{assert.equal(err,null);assert.notStrictEqual(0,result.result.n);}catch(e){console.log(e);result.result = "";};fn( result.result ? [result.result] : []); //如果没报错且返回数据不是0,那么表示操作成功。db.close();});}
var methodType = {// 项目所需login:find,//   type ---> 不放在服务器上面//  放入到服务器//  请求---> 根据传入进来的请求 数据库操作//  req.query    req.bodyshow:find, //后台部分add:add,update:updates,delete:deletes,updatePwd:updates,//portal部分showCourse:find,register:add
};
//主逻辑    服务器  , 请求    --》
// req.route.path ==》 防止前端的请求 直接操作你的数据库
module.exports = function(req,res,collections,selector,fn){MongoClient.connect(Urls, function(err, db) {assert.equal(null, err);console.log("Connected correctly to server");// 根据 请求的地址来确定是什么操作  (为了安全,避免前端直接通过请求url操作数据库)methodType[req.route.path.substr(1)](db,collections,selector,fn);db.close();});};

修改自动生成的 users.js
安装如下模块:
npm i express-session crypto mongodb@2.2.33
在dbhander.js中配置了login对应的操作是查询,返回数据放到数组中。如果数组空,就表示没查到数据,如果非空,比较密码是否一致,如果都正确,就返回登录成功

// routers/users.jsvar express = require('express');
var router = express.Router();
var handler = require('./dbhandler');
var crypto = require('crypto'); // crypto是加密包,对传输过来的密码进行加密/* GET users listing. */
router.get('/', function(req, res, next) {res.send('respond with a resource');
});// 登录
router.post('/login', (req, res, next) => {var md5 = crypto.createHash('md5');var password = md5.update(req.body.password).digest('base64');handler(req, res, "users", {name: req.body.username}, (data) => {console.log(data)if (data.length === 0) {res.end('{"err": "抱歉,系统中并无该用户,如有需要,请向管理员申请"}');} else if (data[0].password !== password) {res.end('{"err": "密码不正确"}');} else if (data.length !== 0 && data[0].password === password) {req.session.username = req.body.username; // 存sessionreq.session.password = password;res.end('{"success": "true"}');}})
})module.exports = router;

这样请求的代码就写完了,但是跨域请求 需要在node中也作配置才可以请求到

修改app.js,在11行左右找到 var app= express(),在其后面添加如下代码

第二段代码是服务器端存session的,直接使用express-session模块,然后添加配置项即可(配置项的说明在备注中)

// app.js// 跨域(后面上线的时候需要删掉)
app.all('*', (req, res, next) => {res.header('Access-Control-Allow-Origin', "http://localhost:8088"); // 为了跨域保持session,需指定地址,不能用*res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');res.header("Access-Control-Allow-Headers", "X-Requested-With");res.header('Access-Control-Allow-Headers', 'Content-Type');res.header('Access-Control-Allow-Credentials', true); next();
})// session
var session = require('express-session');
app.use(session({secret: 'classweb0731', // 设置session签名name: 'classweb',cookie: {maxAge: 60 * 1000 * 60 * 24}, // 存储时间 24 小时resave: false, // 每次请求都重新设置sessionsaveUninitialized: true
}))

// server架构
.
|-- app.js
|-- bin
|   `-- www
|-- node_modules // 省略
|-- package-lock.json
|-- package.json
|-- public
|   |-- images
|   |-- javascripts
|   `-- stylesheets
|-- routes
|   |-- dbhandler.js
|   |-- index.js
|   `-- users.js
|-- tree.txt
`-- views|-- error.jade|-- index.jade`-- layout.jade117 directories, 11 files
// vueclient.
|-- README.md
|-- build
|   |-- build.js
|   |-- check-versions.js
|   |-- logo.png
|   |-- utils.js
|   |-- vue-loader.conf.js
|   |-- webpack.base.conf.js
|   |-- webpack.dev.conf.js
|   `-- webpack.prod.conf.js
|-- config
|   |-- dev.env.js
|   |-- index.js
|   `-- prod.env.js
|-- index.html
|-- node_modules // 省略。。。
|-- package-lock.json
|-- package.json
|-- src
|   |-- App.vue
|   |-- assets
|   |-- components
|   |-- main.js
|   `-- router
|-- static
`-- tree.txt759 directories, 18 files

二、后台路由,导航,首页,退出登录

2.1 首页导航 路由配置

上面我们已经实现了登录功能,那么接着我就需要写登录完成后跳转的页面
项目中需要一个字体图标库 fontawesome,
下载地址:http://fontawesome.dashgame.com/
下载好以后把css和font放到static中,然后我们在index.html中引入

.// static结构
|-- css
|   `-- font-awesome.min.css
`-- fonts|-- FontAwesome.otf|-- fontawesome-webfont.eot|-- fontawesome-webfont.svg|-- fontawesome-webfont.ttf|-- fontawesome-webfont.woff`-- fontawesome-webfont.woff2

// 根目录下index.html<link rel="stylesheet" type="text/css" href="../static/css/font-awesome.min.css" />

注: 为什么是 ../static 这样去找static,而不是 ./ ,因为当进入二级路由以后,在路由内部index和static就不再被认为是同一级,就找不到了,所以就通过 ../往上再找了一级

我们要设置一些统一的全局样式,我们就直接写在 index.html中,这里本来不是一次就写完这些样式,但为了避免以后再回来添加样式,这里就一起写了,首先清楚了全局的margin等,然后定义了 .btn按钮样式 .myinput输入框样式,以后再使用

// index.html<style>*{margin: 0;padding: 0;}body{font-size: 14px;font-family: arial "microsoft yahei";background: #f0f2f5;}ul,li{list-style: none;}/*按钮*/.btn{border:1px solid #4187db;color: #4187db;background: #fff;padding: 6px 14px 7px;border-radius: 3px;transition: all 0.5s ease;outline: none;margin-top: 14px;cursor: pointer;}.btn i{margin-right: 4px;}.btn:hover{background: #4187db;color: #fff;}/*输入框*/.myinput{width: 65%;border: 1px solid #cad3de;height: 35px;line-height: 35px;margin: 5px 0 10px;border-radius: 3px;padding: 0 10px;outline: none;box-sizing: border-box;}.myinput:focus{border: 1px solid #4289dc;}
</style>

assets文件夹中创建 images文件夹,放入我们backIndex.vue中需要的图片

修改路由文件 index.js,并且在components中创建 backIndex.vue组件

// router/index.jsimport Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'
import BackIndex from '@/components/BackIndex'Vue.use(Router)export default new Router({routes: [{path: '/',name: 'Login',component: Login},{path: '/backIndex', // 首页框架name: 'BackIndex',component: BackIndex}]
})// BackIndex.vue<template><div></div>
</template>

在BackIndex.vue组件中写入后面代码
基本功能如下图,左侧导航,顶部搜索栏和个人头像 退出等操作

// BackIndex.vue页面骨架<template><div class="backlogin"><!-- 顶部 --><div class="header"><div class="search_box"><i class="fa fa-search" aria-hidden="true"></i><input type="text"></div><div class="handler"><div class="more"><i class="fa fa-bars" aria-hidden="true"></i><ul><li><a href="javascript:;"><i class="fa fa-sign-out" aria-hidden="true"></i></a></li><li><a href="javascript:;">修改密码</a></li><li><a href="javascript:;">意见反馈</a></li></ul></div><img src="../assets/images/teacherimg01.png" alt="" /></div></div><!-- 侧面导航 --><div class="sidenav_box"><img src="../assets/images/logo03.png" alt="" class="logo" /><ul class="sidenav"><li><router-link to="/backIndex/indexContent"><i class="fa fa-home" arial-hidden="true"></i><span>网站首页</span></router-link></li><li><router-link to="/backIndex/adminList"><i class="fa fa-user-o" arial-hidden="true"></i><span>后台人员</span></router-link></li><li><router-link to="/backIndex/studentList"><i class="fa fa-user-circle-o" arial-hidden="true"></i><span>学员管理</span></router-link></li><li><router-link to="/backIndex/courseList"><i class="fa fa-book" arial-hidden="true"></i><span>课程管理</span></router-link></li></ul></div><!-- 内容区 --><div class="content"><ul class="breadcrumb"><li><a href="#/backIndex">首页</a></li><li>网站首页</li></ul><!-- <router-view></router-view> --></div></div>
</template>
// BackIndex.vue<template><div class="backlogin"><!-- 顶部 --><div class="header"><div class="search_box" :class="{ search_box_fouce: search_box_fouce }"><i class="fa fa-search" aria-hidden="true"></i><input type="text" placeholder="搜索..." @focus="focusFn" @blur="blurFn" /></div><div class="handler"><div class="more" @click="toggleSlide"><i class="fa fa-bars" aria-hidden="true"></i><ul :class="{ showul: showExit }"><li><a href="javascript:;" @click="logout"><i class="fa fa-sign-out" aria-hidden="true"></i>退出</a></li><li><a href="javascript:;">修改密码</a></li><li><a href="javascript:;">意见反馈</a></li></ul></div><img src="../assets/images/teacherimg01.png" alt="" /></div></div><!-- 侧面导航 --><div class="sidenav_box"><img src="../assets/images/logo03.png" alt="" class="logo" /><ul class="sidenav"><li><router-link to="/backIndex/indexContent"><i class="fa fa-home" arial-hidden="true"></i><span>网站首页</span></router-link></li><li><router-link to="/backIndex/adminList"><i class="fa fa-user-o" arial-hidden="true"></i><span>后台人员</span></router-link></li><li><router-link to="/backIndex/studentList"><i class="fa fa-user-circle-o" arial-hidden="true"></i><span>学员管理</span></router-link></li><li><router-link to="/backIndex/courseList"><i class="fa fa-book" arial-hidden="true"></i><span>课程管理</span></router-link></li></ul></div><!-- 内容区 --><div class="content"><ul class="breadcrumb"><li><a href="#/backIndex">首页</a></li><li>{{ pageTitle }}</li></ul><router-view></router-view></div></div>
</template>
<script>var pageTitleObj = {indexContent: '网站首页',adminList: '后台人员',studentList: '学员管理',courseList: '课程管理',courseEdit: '课程编辑'}export default {name: "backlogin",data () {return {search_box_fouce: false,showExit: false,pageTitle: pageTitleObj[this.$route.path.substr(this.$route.path.lastIndexOf('/') + 1)] || "网站首页"}},methods: {// 搜索框获取焦点,添加classfocusFn () {this.search_box_fouce = true},// 搜索框失去焦点,去掉classblurFn () {this.search_box_fouce = false},// 头像旁边下拉框的显示与隐藏toggleSlide () {this.showExit = !this.showExit},// 退出系统logout () {}},watch: {$route: {handler (val, oldVal) {var path = val.path;this.pageTitle = pageTitleObj[path.substr(path.lastIndexOf("/") + 1)] || "网站首页"}}}}
</script>
<style scoped>ul, li {list-style: none;}
/* 顶部栏 */.header {height: 60px;box-shadow: 0 1px 5px rgba(13, 62, 73, .2);background: #fff;margin-left: 80px;min-width: 740px;}.search_box {color: #979fa8;padding-top: 20px;float: left;}.search_box i {margin: 0 12px 0 70px;transition: all 0.5s ease;}.search_box input {border: none;outline: none;}.search_box_fouce i {margin-left: 55px;color: #2c3d50;}.handler > * {float: right;margin-right: 20px;cursor: pointer;}.handler .more {font-size: 20px;color: #566a80;margin: 15px 30px 0 0;position: relative;}.handler .more:hover {color: #2c3d50;}.handler .more ul {font-size: 14px;position: absolute;right: 0;top: 55px;width: 120px;box-shadow: 0 1px 5px rgba(13, 62, 73, .2);transition: all 0.3s ease-out;height: 0;opacity: 0;overflow: hidden;text-align: center;}.handler .more .showul {height: auto;top: 45px;opacity: 1;border-top: 1px solid #979fa8;}.handler .more a {display: block;padding: 8px 10px;background: #fff;color: #566a80;text-decoration: none;}.handler .more a:hover {background: #f8f9fb;}.handler > img {width: 50px;border-radius: 50%;margin-top: 5px;margin-right: 30px;}
/* 侧边栏 */.sidenav_box {width: 80px;box-shadow: 0 1px 5px rgba(13, 62, 73, .2);position: fixed;left: 0;top: 0;bottom: 0;background: #fff;z-index: 99;}.sidenav_box .logo {width: 46px;margin: 20px 0 0 17px;}.sidenav {margin-top: 30px;}.sidenav li {margin-bottom: 20px;}.sidenav a {display: block;width: 56px;height: 56px;margin: 0 auto;position: relative;cursor: pointer;opacity: 0.6;transition: all 0.5s ease;text-decoration: none;}.sidenav a:hover {background: #f0f2f5;opacity: 1;}.sidenav a i {display: block;font-size: 20px;line-height: 56px;text-align: center;color: #566a80;}.sidenav a span {position: absolute;left: 55px;top: 22px;background: #000;color: #fff;width: 0;padding: 5px 0;border-radius: 3px;font-size: 12px;opacity: 0;}.sidenav a span:after {content: "";position: absolute;top: 8px;left: -10px;border: 5px solid transparent;border-right-color: #000;}.sidenav a:hover span {opacity: 1;left: 65px;width: 60px;padding: 5px 20px;transition: none 0.5s ease-out;transition-property: opacity, left;}.sidenav .router-link-active {opacity: 1;background: #f0f2f5;}.sidenav .router-link-active:after {content: "";position: absolute;left: -16px;top: 8px;height: 40px;width: 8px;border-radius: 3px;background: #566a80;}
/* 主页内容 */.content {margin: 20px 30px 0px 100px;min-height: 300px;min-width: 700px;}.breadcrumb {border-radius: 4px;padding: 10px 15px;background: #fff;}.breadcrumb > li {display: inline-block;color: #777;}.breadcrumb > li + li:before {padding: 0 5px;color: #ccc;content: "/\00a0";}.breadcrumb > li > a {color: #32475f;text-decoration: none;}</style>

在地址栏输入 http://localhost:8088/#/backIndex 就可以看到首页框架的效果了。 (这时候内部页面还没有,所以点击左侧导航会找不到页面,先不要点)

下面继续将所有的路由配置其他页面的路由

// indext.jsimport Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'
import BackIndex from '@/components/BackIndex' // 首页框架
import CourseList from '@/components/CourseList' // 课程列表
import IndexContent from '@/components/IndexContent' // 首页统计
import AdminList from '@/components/AdminList' // 后台用户
import StudentList from '@/components/StudentList' // 学员用户
import CourseEdit from '@/components/CourseEdit' // 编辑课程Vue.use(Router)export default new Router({routes: [{path: '/',name: 'Login',component: Login},{path: '/backIndex', // 首页框架name: 'BackIndex',component: BackIndex,children: [{ path: 'courseList', component: CourseList }, // 课程列表{ path: 'indexContent', component: IndexContent }, // 首页统计{ path: 'adminList', component: AdminList }, // 后台用户{ path: 'studentList', component: StudentList }, // 学员用户{ path: 'courseEdit', component: CourseEdit }, // 编辑课程{ path: '*', redirect: 'indexContent' }]}]
})
添加如下相应页面视图模板
CourseList from  // 课程列表
IndexContent from  // 首页统计
AdminList from  // 后台用户
StudentList  // 学员用户
CourseEdit  // 编辑课程

// 以上视图模板内容如下:
<template><div></div>
</template>

再刷新页面的时候,左侧导航就可以点击了

2.2 首页统计页面

下面为 indexContent.vue 添加中间显示的统计图表,代码在后面

canvas图表详解系列(2):折线图
http://www.cnblogs.com/chengduxiaoc/p/7678967.html

// indexContent.vue<template><div class="indexContent main"><h4>最新数据</h4><ul class="number"><li><div class="title">今日访问</div><p>12000</p><a href="javascript:;">查看详情<i class="fa fa-angle-right" aria-hidden="true"></i></a></li><li><div class="title">学员总数</div><p>3000000</p><a href="javascript:;">查看详情<i class="fa fa-angle-right" aria-hidden="true"></i></a></li><li><div class="title">在学人数</div><p>2000</p><a href="javascript:;">查看详情<i class="fa fa-angle-right" aria-hidden="true"></i></a></li></ul><canvas id="barChart" height="400" width="600" style="margin:10px 0"> 你的浏览器不支持HTML5 canvas </canvas></div>
</template><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>.main{border-radius: 4px;background: #fff;margin-top: 10px;overflow: hidden;}.main > h4{color: #51555a;padding:10px;border-bottom: 1px solid #DFE3EA;}.number{width: 30%;float: right;margin-right: 10%;margin-top: 10px;color: #566A80;}.number li{padding: 20px;border-top:1px solid #F0F2F5;}.number li:first-child{border: none 0;}.number p{font-size: 20px;font-family: arial;margin: 10px 0;}.number a{text-decoration: none;color: #4187db;font-size: 12px;}.number li:hover{color: #173859;}.number a:hover{}.number i{transition: all 0.3s ease-out;padding-left: 10px;}.number a:hover i{padding-left: 20px;}.number:hover li{border-color:#DFE3EA}canvas{max-width: 55%;min-width: 45%;}
</style><script>export default {name: 'indexContent',data () {return {}},methods:{},mounted:function(){var chartData = [["2017/01", 50], ["2017/02", 60], ["2017/03", 100], ["2017/04",200], ["2017/05",350], ["2017/06",600]];goBarChart(chartData);}}function goBarChart(dataArr){// 声明所需变量var canvas,ctx;// 图表属性var cWidth, cHeight, cMargin, cSpace;var originX, originY;// 折线图属性var tobalDots, dotSpace, maxValue;var totalYNomber;// 运动相关变量var ctr, numctr, speed;// 获得canvas上下文canvas = document.getElementById("barChart");if(canvas && canvas.getContext){ctx = canvas.getContext("2d");}initChart(); // 图表初始化drawLineLabelMarkers(); // 绘制图表轴、标签和标记drawBarAnimate(); // 绘制折线图的动画//点击刷新图表canvas.onclick = function(){initChart(); // 图表初始化drawLineLabelMarkers(); // 绘制图表轴、标签和标记drawBarAnimate(); // 绘制折线图的动画};// 图表初始化function initChart(){// 图表信息cMargin = 60;cSpace = 80;canvas.width = Math.floor( (window.innerWidth-100)/2 ) * 2 ;canvas.height = 740;canvas.style.height = canvas.height/2 + "px";canvas.style.width = canvas.width/2 + "px";cHeight = canvas.height - cMargin - cSpace;cWidth = canvas.width - cMargin - cSpace;originX = cMargin + cSpace;originY = cMargin + cHeight;// 折线图信息tobalDots = dataArr.length;dotSpace = parseInt( cWidth/tobalDots );maxValue = 0;for(var i=0; i<dataArr.length; i++){var dotVal = parseInt( dataArr[i][1] );if( dotVal > maxValue ){maxValue = dotVal;}}maxValue += 50;totalYNomber = 10;// 运动相关ctr = 1;numctr = 100;speed = 6;ctx.translate(0.5,0.5);  // 当只绘制1像素的线的时候,坐标点需要偏移,这样才能画出1像素实线}// 绘制图表轴、标签和标记function drawLineLabelMarkers(){ctx.font = "24px Arial";ctx.lineWidth = 2;ctx.fillStyle = "#566a80";ctx.strokeStyle = "#566a80";// y轴drawLine(originX, originY, originX, cMargin);// x轴drawLine(originX, originY, originX+cWidth, originY);// 绘制标记drawMarkers();}// 画线的方法function drawLine(x, y, X, Y){ctx.beginPath();ctx.moveTo(x, y);ctx.lineTo(X, Y);ctx.stroke();ctx.closePath();}// 绘制标记function drawMarkers(){ctx.strokeStyle = "#E0E0E0";// 绘制 y 轴 及中间横线var oneVal = parseInt(maxValue/totalYNomber);ctx.textAlign = "right";for(var i=0; i<=totalYNomber; i++){var markerVal =  i*oneVal;var xMarker = originX-5;var yMarker = parseInt( cHeight*(1-markerVal/maxValue) ) + cMargin;//console.log(xMarker, yMarker+3,markerVal/maxValue,originY);ctx.fillText(markerVal, xMarker, yMarker+3, cSpace); // 文字if(i>0){drawLine(originX+2, yMarker, originX+cWidth, yMarker);}}// 绘制 x 轴 及中间竖线ctx.textAlign = "center";for(var i=0; i<tobalDots; i++){var markerVal = dataArr[i][0];var xMarker = originX+i*dotSpace;var yMarker = originY+30;ctx.fillText(markerVal, xMarker, yMarker, cSpace); // 文字if(i>0){drawLine(xMarker, originY-2, xMarker, cMargin    );}}// 绘制标题 yctx.save();ctx.rotate(-Math.PI/2);ctx.fillText("访问量", -canvas.height/2, cSpace-10);ctx.restore();// 绘制标题 xctx.fillText("月份", originX+cWidth/2, originY+cSpace/2+20);};//绘制折线图function drawBarAnimate(){ctx.strokeStyle = "#566a80";  //"#49FE79";//连线ctx.beginPath();for(var i=0; i<tobalDots; i++){var dotVal = dataArr[i][1];var barH = parseInt( cHeight*dotVal/maxValue* ctr/numctr );//var y = originY - barH;var x = originX + dotSpace*i;if(i==0){ctx.moveTo( x, y );}else{ctx.lineTo( x, y );}}ctx.stroke();//背景ctx.lineTo( originX+dotSpace*(tobalDots-1), originY);ctx.lineTo( originX, originY);//背景渐变色//柱状图渐变色var gradient = ctx.createLinearGradient(0, 0, 0, 300);gradient.addColorStop(0, 'rgba(133,171,212,0.6)');gradient.addColorStop(1, 'rgba(133,171,212,0.1)');ctx.fillStyle = gradient;ctx.fill();ctx.closePath();ctx.fillStyle = "#566a80";//绘制点for(var i=0; i<tobalDots; i++){var dotVal = dataArr[i][1];var barH = parseInt( cHeight*dotVal/maxValue * ctr/numctr );var y = originY - barH;var x = originX + dotSpace*i;drawArc( x, y );  //绘制点ctx.fillText(parseInt(dotVal*ctr/numctr), x+15, y-8); // 文字}if(ctr<numctr){ctr++;setTimeout(function(){ctx.clearRect(0,0,canvas.width, canvas.height);drawLineLabelMarkers();drawBarAnimate();}, speed);}}//绘制圆点function drawArc( x, y, X, Y ){ctx.beginPath();ctx.arc( x, y, 3, 0, Math.PI*2 );ctx.fill();ctx.closePath();}}</script>

2.3 登录功能完善

登录请求完成以后,如果出错,就弹出错误(我们本项目没有封装模态框,就直接用alert吧),
如果正确,就跳转到首页

修改后的 login.vue中的 ajax请求代码如下:

    login () {this.disablebtn = truethis.loginText = "登录中..."this.$axios.post('/users/login', {username: this.username,password: this.password}).then((result) => {// 成功// console.log(result);if (result.data.err) {alert(result.data.err);} else {this.$router.push({path: '/backIndex/indexContent'})}this.disablebtn = falsethis.loginText = "登录"}).catch((error) => {// 失败this.disablebtn = falsethis.loginText = "登录"})}

注:我们通过 router.push去修改url,作用和原生js的 window.location.href基本一致

2.4 退出系统

BackIndex.vue中我们有一个退出登录的空方法,我们在里面写退出登录的请求的代码,退出成功后跳转到根目录

      // 退出系统logout () {this.$axios.post('/users/logout', {}).then((result) => {this.$router.push({path: '/'});}).catch((error) => {console.log(error);})}

然后在后台写接口,在user.js中 登录的方法后面写(修改完成后需要重启node服务)

注:这里直接清除登录中设置的 session 就可以了,(我们后面会对所有的请求设置拦截,如果session中的用户信息没有,再提示用户未登录,跳转到登录页面就可以了)

// 退出
router.post('/logout', (req, res, next) => {req.session.username = ""; // 清除sessionreq.session.password = "";res.end('{"success": "true"}');
})

到此,我们就实现了登录,显示首页,退出的基本功能

三、用户添加/修改/删除 vue表格组件 vue分页组件

3.1 用户添加/修改/删除 表格组件 分页组件

由于要用到表格,我们这里就得封装 表格和分页组件
先在componets中创建分页组件 pagebar.vue,写入以下代码(功能是传入分页信息,然后展示分页,点击分页的时候,会向上触发goto()跳转到第几页

// pageBar.vue<template><div><ul class="pagination"><li :class="{hideLi: current == 1}" @click="goto(current - 1)"><a href="javascript:;" arial-label="Previous"><span arial-hidden="true">&laquo;</span></a></li><li v-for="(index, key) in pages" @click="goto(index)" :key="index" :class="{'active': current == index}"><a href="javascript:;">{{ index }}</a></li><li :class="{hideLi: (allpage == current || allpage == 0)}" @click="goto(current + 1)"><a href="javascript:;" arial-label="Next"><span arial-hidden="true">&raquo;</span></a></li></ul></div>
</template>
<script>/*** 分页组件* 设置props*    current 当前页 默认1*    showItem 显示几页 默认5*    allpage 总页数 10*/export default {name: 'page',data () {return {}},props: {current: {type: Number,default: 1},showItem: {type: Number,default: 5},allpage: {type: Number,default: 10}},computed: {pages () {var pag = [];if (this.current < this.showItem) {var i = Math.min(this.showItem, this.allpage);while (i) {pag.unshift(i--);}} else {var middle = this.current - Math.floor(this.showItem / 2),i = this.showItem;if (middle > (this.allpage - this.showItem)) {middle = (this.allpage - this.showItem) + 1}while (i--) {pag.push(middle++);}}return pag;}},methods: {goto (index) {if (index == this.current) return;this.$emit('on-gopage', index);}}}
</script>
<style scoped>.pagination {margin: 10px;display: inline-block;}.pagination li {display: inline;}.pagination li a, .pagination li span {float: left;padding: 6px 12px;margin-left: -1px;line-height: 1.42857143;color: #4187db;text-decoration: none;background: #fff;border: 1px solid #f8f9fb;}.pagination li a:hover {background-color: #f8f9fb;}.pagination .active a {background-color: #4187db !important;color: #fff;}.hideLi a {visibility: hidden;}
</style>
// Grid.vue<template><div><table cellspacing="" cellpadding="" border=""><thead><tr><th>序号</th><th v-for="(item, index) in theadData">{{ item.title }}</th></tr></thead><tbody><tr v-if="!listData.length"><td>1</td><td>没有数据... ...</td><td v-for="(item, index) in theadData" v-if="index <= theadData.length - 2"></td></tr><tr v-for="(item, index) in listData"><td>{{ index + 1 }}</td><td v-for="(item2, index2) in theadData"><span v-if="index2 === 0" style="float: right;"><i title="编辑" v-if="ifEdit" class="fa fa-edit" aria-hidden="true" @click="editHandler(item)"></i><i title="删除" v-if="ifDelete" class="fa fa-trash" aria-hidden="true" @click="deleteHandler(item)"></i><i title="下移" v-if="ifDown" class="fa fa-arrow-circle-o-down" aria-hidden="true" @click="downHandler(item)"></i><i title="上移" v-if="ifUp" class="fa fa-arrow-circle-o-up" aria-hidden="true" @click="upHandler(item)"></i><i title="封号" v-if="ifReset" class="fa fa-unlock-alt" aria-hidden="true" @click="resetHandler(item)"></i></span>{{ item[item2.keyname] }}</td></tr></tbody></table><pagebar v-if="ifpage" :current="pageInfo.current" :showItem="pageInfo.showItem" :allpage="pageInfo.allpage"@on-gopage="gopage"></pagebar></div>
</template>
<script>
/*** 表格组件* 设置props*    theadData 表头数据 默认[]*    listData 表格数据 默认[]*    ifpage  是否分页 默认true*    isEdit/ifDelete/ifUp/ifDown 是否可编辑/删除/上下移动 默认false* * 定制模板*    slot为grid-thead 定制表格头部*    slot为grid-handler 定制表格操作* * 监听状态变化*    on-delete 删除*    on-edit   编辑*    on-up     上移*    on-down   下移* * 分页* pageInfo 分页信息如下 默认{} -- 或单独使用pagebar.vue* {*    current: 当前第几页 默认1*    showItem: 显示多少页 默认5*    allpage:总页数   默认10* }*/
import pagebar from './pagebar'
export default {name: 'grid',data () {return {}},props: {listData: {type: Array,// default: function () {default () {return [{name: "没有数据..."}]}},theadData: {type: Array,// default: function () {default () {return [{title: "名字",keyname: "name"}]}},ifpage: {type: Boolean,default: true},ifEdit: {type: Boolean,default: false},ifDelete: {type: Boolean,default: false},ifUp: {type: Boolean,default: false},ifDown: {type: Boolean,default: false},ifReset: {type: Boolean,default: false},pageInfo: {type: Object,default: function () {return {}}}},methods: {editHandler (item) {this.$emit('on-edit', item)},deleteHandler (item) {this.$emit('on-delete', item)},downHandler (item) {this.$emit('on-down', item)},upHandler (item) {this.$emit('on-up', item)},resetHandler (item) {this.$emit('on-reset', item)},gopage (index) {this.$emit('on-gopage', index)}},components: {pagebar}
}
</script>
<style scoped>table {border: none 0;border-collapse: collapse;color: #51555a;width: 100%;border-bottom: 1px solid #dfe3ea;}td, th {padding: 10px 20px;text-align: left;border-width: 0;}thead tr, tr:nth-of-type(even) {background: #f8f9fb;}td .fa {padding: 0 5px;cursor: pointer;opacity: 0;transition: all 0.3s ease;}td .fa:first-child {margin-left: 10px;}tr:hover .fa {opacity: 1;}td .fa:hover {color: #4187db;transform: scale(1.2);}
</style>
// AdminList.vue<template>
<div class="adminList main"><div class="input_box"><input class="myinput" type="text" placeholder="用户名" v-model="Admin.name" /><input class="myinput" type="text" placeholder="手机号" v-model="Admin.phone" /><input class="myinput" type="password" placeholder="密码" v-if="!editAdminOjb" v-model="Admin.password" /><button class="btn" v-if="!editAdminOjb" @click="addAdmin()"><i class="fa fa-plus" aria-hidden="true"></i>添加</button><button class="btn" v-if="!editAdminOjb" @click="saveEditAdmin()"><i class="fa fa-save" aria-hidden="true"></i>保存</button><button class="btn" v-if="!editAdminOjb" @click="cancelEditAdmin()" style="opacity: 0.8;"><i class="fa fa-times-circle-o" aria-hidden="true"></i>取消</button></div><grid :listData="listData":theadData="theadData":ifEdit="true":ifDelete="true":ifpage="true":pageInfo="pageInfo"@on-delete="deleteAdmin"@on-edit="editAdmin"@on-gopage="gopage"></grid>
</div>
</template>
<script>var theadData = [{ title: '用户名', keyname: 'name'},{ title: '手机号', keyname: 'phone'}];import grid from './grid'export default {name: 'adminList',components: { grid },data () {return {listData: [],theadData: theadData,Admin: { // 用户信息name: "",phone: "",password: ""},editAdminOjb: null, // 用于存放正在编辑的用户pageInfo: {}}},mounted () {this.getAdminList(1);},methods: {getAdminList (page) {this.$axios.post('/users/AdminList', {page:page}).then((result) => {// 成功this.listData = result.data.data;this.pageInfo.allpage = Math.ceil(result.data.total / 5);}).catch((error) => {// 失败console.log(error);})},addAdmin () { // 添加用户if (!this.Admin.name || !this.Admin.phone || !this.Admin.password) {alert('不能为空');return false;}this.$axios.post('/users/add', this.Admin).then((result) => {// 成功this.getAdminList();this.emptyAdmin();}).catch((error) => {// 失败console.log(error);})},editAdmin (item) { // 编辑用户this.editAdminOjb = item;this.Admin = JSON.parse(JSON.stringify(item));},saveEditAdmin () {if (!this.Admin.name || !this.Admin.phone) {alert('不能为空');return false;}this.$axios.post('/users/update', this.Admin).then((result) => {// 成功this.gopage(this.pageInfo.current);this.editAdminOjb = null;this.emptyAdmin();}).catch((error) => {// 失败console.log(error);})},cancelEditAdmin () {this.editAdminOjb = null;this.editAdmin();},emptyAdmin () { // 清空输入框this.Admin.name = "";this.Admin.phone = "";this.Admin.password = "";},deleteAdmin (item) {this.$axios.post('/users/delete', item).then((result) => {// 成功this.gopage(this.pageInfo.current);this.emptyAdmin();}).catch((error) => {// 失败console.log(error);})},gopage (index) {this.pageInfo.current = index;// 查询数据this.getAdminList(index);}}}
</script>
<style scoped>.main {border-radius: 4px;background: #fff;margin-top: 10px;}.input_box {padding: 0 10px;}.input_box .myinput {width: 25%;}
</style>

添加增删改用户的接口

于需要对 _id进行转化,我们还需要引入mongodb的ObjectId模块

// routes/users.js
var ObjectId = require('mongodb').ObjectId;// 管理员列表
router.post('/AdminList', (req, res, next) => {req.route.path = '/page'; // 修改page来设定对数据库的操作var page = req.body.page || 1;var rows = req.body.rows || 5;handler(req, res, "users", [{}, {limit: rows, skip: (page - 1) * rows}], (data, count) => {var obj = {data: data,total: count,success: '成功'};var str = JSON.stringify(obj);res.end(str);})})// 添加管理员
router.post('/add', (req, res, next) => {var md5 = crypto.createHash('md5');req.body.password = md5.update(req.body.password).digest('base64');handler(req, res, "users", req.body, (data) => {if (data.length == 0) {res.end('{"err": "抱歉,添加失败"}');} else {res.end('{"success": "添加成功"}');}})
})// 删除用户
router.post('/delete', (req, res, next) => {handler(req, res, "users", {"_id": ObjectId(req.body._id)}, (data) => {// console.log(data);if (data.length == 0) {res.end('{"err": "抱歉,删除失败"}');} else {var obj = {success: '删除成功'};var str = JSON.stringify(obj);res.end(str);}})
})// 编辑更新用户
router.post('/update', (req, res, next) => {var selectors = [{"_id": ObjectId(req.body._id)},{"$set": {name: req.body.name,phone: req.body.phone }}];handler(req, res, "users", selectors, (data) => {if (data.length == 0) {res.end('{"err": "抱歉,修改失败"}');} else {res.end('{"success": "修改成功"}');}})
})

响应拦截

不登陆也能请求列表数据,需要对所有的请求进行拦截,只有当登录了,才能请求数据

// app.js   session后面添加内容// 验证用户登录
app.use((req, res, next) => {// 后台请求if (req.session.username) { // 表示已登录后台next();} else if (req.url.indexOf("login") >= 0 || req.url.indexOf("logout") >= 0) {next(); // 登入、登出,不需要登录} else {res.end('{"redirect": "true"}')}
})

然后在vue的main.js中 作redirect跳转,还有当后台返回err的处理
这里在axios中作响应前拦截,就是所有的响应到达$req.post的then(){}之前执行的代码,具体的axios配置项大家可以查查axios官网

// main.js// 添加响应拦截器
axios.interceptors.response.use((response) => {// 处理响应数据if (response.data.err) {alert(response.data.err);return Promise.reject(response);} else if (response.data.redirect) {alert('请先登录...');window.location.href = '#/'; // 跳转到登录页return Promise.reject(response);} else {return response; // 返回response后继续执行后面的操作}
}, (error) => {// 对错误处理做一些响应return Promise.reject(error);
})

Node+Vue实战项目相关推荐

  1. vue实战项目:电商管理系统实现步骤笔记(一)

    vue实战项目 视频地址以及项目文件 一.项目概述 1.1电商项目基本业务概述 1.2电商后台管理系统的功能 1.3电商后台管理系统的开发模式(前后端分离) 1.4电商后台管理系统的技术选型 1.4. ...

  2. 推荐一些vue实战项目

    好的,下面是一些推荐的 Vue 实战项目: 待办事项应用:这是一个简单的 Vue 项目,可以帮助你熟悉 Vue 的基础知识,并学习如何使用 Vue 实现增删改查功能. 电商网站:这是一个更复杂的 Vu ...

  3. android项目实战博学谷源码_Vue框架:史上最详细的Vue实战项目之喵喵电影(视频+源码)...

    Vue是web前端中重要的框架之一,与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计,Vue 的核心库只关注视图层,并且非常容易学习,非常容易与其它库或已有项目整合.所以,对于web前端开 ...

  4. 高仿网易云音乐(vue实战项目)

    高仿网易云音乐(Vue实战项目)

  5. 【Vue 实战项目】后台管理系统登录页详解附源码

    提示:前端查漏补缺,仅代表个人观点. 文章目录 一.先看效果图 二.实战步骤 1. 创建项目 2. 引入库 3. 登录页关键代码 三.页面源代码 总结 提示:项目源代码除了登录页面还有动态路由 一.先 ...

  6. 初级项目vue实战项目 收银系统

    Vue实战视频-快餐店收银系统 (共11集) 2017-05-22 分类:Vue.js视频教程 / 视频教程 阅读(126832) 评论(136)  课程前言 这是我网站恢复后写的第一篇文章,在关站这 ...

  7. vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇

    文章目录 最好使用视频上的账号密码,13700000000 密:111111 最新服务端接口地址:http://gmall-h5-api.atguigu.cn 脚手架使用 1.创建项目 2.脚手架默认 ...

  8. Vue实战项目个人总结

    根据网上别人提供的视频,我模仿着做了一个适用于手机端的电影网站,参考链接为: https://www.bilibili.com/video/BV1u4411Y7t4?p=5 开发项目流程 1.项目需求 ...

  9. node vue 合并项目_吐血整理最佳实践:SpringBoot整合Vue前后端分离开发

    [toc] 开发环境 IDEA V2018.5 npm v6.4.x vue-cli v2.9.x 创建项目 IDEA > Create New Project > Gradle 2018 ...

最新文章

  1. GDI+中发生一般性错误
  2. 操作系统二:计算机内存体系
  3. 一文搞懂C语言typedef关键字
  4. PHP 获取页面地址参数详解整理
  5. ESDF建图库voxblox的安装编译过程
  6. 11.2 正睿停课训练 Day15
  7. 链表反转(递归与非递归实现)
  8. C语言 - 详解回调函数
  9. 互联网公司纷纷裁员,大家都在说互联网行业进入了寒冬期,你怎么看待这个说法?
  10. 厦门大学继续教育计算机科学毕业难吗,厦门大学工资待遇
  11. 互联网是如何工作的?
  12. 在js中为什么0.1+0.2不等于0.3
  13. 乒乓球基本站姿站位和步伐
  14. 思科ccie和华为hcie中交换机环路的产生原因和解决方法
  15. UE4 UDP是如何进行可靠传输的
  16. English--定语从句
  17. vue如何对接网易云信IM即时聊天
  18. 初识:神经网络(Neural Networks)
  19. DbContext 查询(三)
  20. 宽带连接错误691,623,678......

热门文章

  1. C#调用sql存储过程
  2. PLSQL登陆时一直处于logging on
  3. 跨境互联网券商架构最佳实践\n
  4. Arcgis更换布局模板_适合个人的网站导航静态模板
  5. lnk306dn引脚功能_LNK306DN LED卤素灯驱动器开关电源
  6. win7安装oracle 黑屏,雨林木风 win7安装黑屏怎么解决
  7. 系统重启后,VCS状态ADMIN_WAIT or STALE_ADMIN_WAIT
  8. Oracle11g客户端使用2
  9. linux-网络-nc命令
  10. 尚硅谷Manen2022----①基础概念,基础使用