Node+Vue实战项目
一、Node+Vue实战项目1.1 创建Node项目、Vue项目mkdir classwebcd classweb/express servervue init webpack vueclient.|-- server||-- app.js||-- bin||-- node_modules||-- package-lock.json| ...
一、Node+Vue实战项目
1.1 创建Node项目、Vue项目
mkdir classweb
cd classweb/
express server
vue 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
`-- static
12 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 = true
this.loginText = "登录中..."
this.$axios.post('/users/login', {
username: this.username,
password: this.password
}).then((result) => {
// 成功
console.log(result);
this.disablebtn = false
this.loginText = "登录"
}).catch((error) => {
// 失败
this.disablebtn = false
this.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.js
var 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.body
show: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.js
var 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; // 存session
req.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, // 每次请求都重新设置session
saveUninitialized: 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.jade
117 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.txt
759 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.js
import 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: {
// 搜索框获取焦点,添加class
focusFn () {
this.search_box_fouce = true
},
// 搜索框失去焦点,去掉class
blurFn () {
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.js
import 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 );
}
}
// 绘制标题 y
ctx.save();
ctx.rotate(-Math.PI/2);
ctx.fillText("访问量", -canvas.height/2, cSpace-10);
ctx.restore();
// 绘制标题 x
ctx.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 = true
this.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 = false
this.loginText = "登录"
}).catch((error) => {
// 失败
this.disablebtn = false
this.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 = ""; // 清除session
req.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">«</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">»</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);
})
更多推荐
所有评论(0)