OKR目标管理系统(一)
0 项目名称
基于 Node.js 小程序 实现 OKR 目标管理工具
1-项目导学页
1.1 - 项目介绍
OKR(Objectives and Key Results)全称为“目标和关键成果”,是企业进行目标管理的一个简单有效的系统,能够将目标管理自上而下贯穿到基层。制定OKR的基本方法是:首先,要设定一个“目标”(Objective),这个目标不必是确切的、可衡量的,例如“我想让我的网站更好”;然后,设定若干可以量化的“关键结果”(Key Results),用来帮助自己实现目标,例如“让网站速度加快30%”或者“融入度提升15%”之类的具体目标。
1.2 - 教学目标
本项目为 Node.js 构建 Web API,并使用微信小程序构建前台工具。主要考察对微信小程序项目中的应用能力,分成以下两个部分:
Node.js API 的运用,结合 Koa2 框架输出应用的 API 接口
微信小程序的基本运用,框架、组件以及 API 的运用能力
2项目剖析页
使用 Node.js 的 Koa2 及 微信小程序完成 OKR 管理工具,对每日代办事情已经指定的目标和成就进行关联。项目主要包含以下几个模块:
欢迎页面(用于获取用户信息进行登录)
1、Todo 页面
2、Todo 列表
3、Todo 绑定 Keyresult 页面
4、历史完成代办事件
5、OKR 管理
6、OKR 列表
7、OKR 查看
8、OKR 新建
9、OKR 编辑
产品流程图 (无)
2.2 - 项目拆解
本项目主要分为 12 个任务:
任务一: 环境搭建
主要通过 Koa2 快速搭建 Web 服务框架,配置一个测试的 API ,例如:/api/test 返回 { code : 200 } 即可。
任务二:数据库配置
主要通过原型图和业务需求,获取存储的信息内容,以及其中关系进行数据库的构建和对应表及字段的配置。
任务三:页面模版
主要通过小程序新建页面文件 及配置 app.json 引用每个页面的文件地址。
任务四:页面样式
主要通过配置 app.json 的 pages 页面的顺序完成后台页面的结构与样式。
任务五:用户登录
主要通过小程序 app_key 、app_sercet 来获取用户的 open_id 完成用户信息的记录。
任务六:Todos 代办事项
主要通过 Node.js API 完成相关接口,在小程序中完成 Todes 部分的添加、修改状态以及删除 功能。
任务七:History 完成事项
主要通过 Node.js API 完成相关接口,在小程序中完成历史已完成的 Todes 展示。
任务八:OKR CUD
主要通过 Node.js API 完成相关接口,在小程序中完成 OKR 的新建、列表、编辑、删除功能。
任务九:KR 关联页
主要通过 Node.js API 完成相关接口,在 Todes 页面中完成单项 Todo 与 KR 的绑定的展示、关联、取消关联功能。
任务十:OKR 详情
主要通过 Node.js API 完成相关接口,在小程序中 OKR 完成点击单项目标所展现的 OKR详情。
3-任务详情页
3.1 -环境搭建
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
正因为 Koa 没有捆绑任何中间件,所以我们在开发应用的时候一定要注意自己开发,或者找 Koa 对应的中间件,并且还要注意对应的版本。本次任务中我们需要用到 Koa2 来搭建 Web 服务,通过 koa-router 中间件配置一个测试的 API。
任务要求:
1、使用 Koa2 来搭建 Web 服务。
2、实现全局响应处理模块,捕捉全局错误并配置返回值。
3、实现测试 API 返回 Code200
任务提示:
1、下载安装 koa 、koa-router、koa-bodyparser
初始化项目1
2cd ~/Desktop && mkdir okr_koa && cd okr_koa
npm init
下载相关依赖1
npm install koa koa-router koa-bodyparser axios --save
2、新建 middlewares/response ,try 处理响应结果 catch 处理全局错误信息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
36const debug = require('debug')('koa-app')
/**
* 响应处理模块
*/
module.exports = async function (ctx, next) {
try {
ctx.state.code = 0
ctx.state.data = {}
await next()
// 处理响应结果
// 如果直接写入在 body 中,则不作处理
// 如果写在 ctx.body 为空,则使用 state 作为响应
// ctx.body = {
// code:0,
// data:{}
// }
console.log(ctx.url)
ctx.body = ctx.body ? ctx.body : {
code: ctx.state.code,
data: ctx.state.data
}
} catch (e) {
// catch 住全局的错误信息
debug('Catch Error: %o', e)
// 设置状态码为 200 - 服务端错误
ctx.status = 200
// 输出详细的错误信息
ctx.body = {
code: -1,
error: e && e.message ? e.message : e.toString()
}
}
}
3、新建 controllers/test ,返回 Code200,MessageSuccess1
2
3
4
5
6
7
8
9
10const testController = {
test: async (ctx, next) => {
// ctx.body = 'Hello Koa!'
let user_id = ctx.state.user_id
ctx.state.code = 200;
ctx.state.data = { id: user_id }
}
}
module.exports = testController;
4、把路由的配置分离到单独的 routes
routes/index.js1
2
3
4
5
6
7
8
9const router = require('koa-router')({
prefix: '/api'
})
const testController = require('./../controllers/test.js')
router.get('/test', testController.test)
module.exports = router
5、文件夹中,并配置测试 API
路由需要使用 api 前缀
参考资料:
https://koajs.com/
3.2 - 数据库配置
本次任务我们需要配置数据库的及其表设计。在数据库的配置中,我们首要根据业务需求梳理大框架模块,本次 OKR 项目主要是做什么 ,有哪几个重要的板块 ?项目主要为:用户定义 OKR,然后每天添加代办事项,代办事项和 OKR 关联,根据代办事项的完成状态统计出 OKR 的完成度及状态。
任务要求:
配置数据库,生成 sql 文件,放置在更目录的 db 文件夹中
任务提示:
1、用户信息表 user
2、目标表 objective
3、成就表 keyresult
4、代办事项表 todo
5、代办事项与成就关联表 todo_keyresult
3.3 - 页面模版
本次任务我们需要为小程序配置页面模版,根据需求梳理出共有多少个页面,在 pages 目录下创建对应页面的文件夹和相关文件,并在 app.json 中配置 pages 以及 tabs。
任务要求:
完成一下页面配置:
欢迎页
1、Todo 列表页
2、Todo Keyresult 绑定页
3、Todo 完成历史页
4、OKR 列表页
5、OKR 新建页
6、OKR 编辑页
7、OKR 详情页
任务提示:
“pages/welcome/welcome”
“pages/todo/todo”
“pages/todo_keyresult/todo_keyresult”
“pages/okr/okr”
“pages/okr_edit/okr_edit”
“pages/okr_create/okr_create”
“pages/okr_detail/okr_detail”
“pages/history/history”
微信小程序 app.json1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
"pages": [
"pages/welcome/welcome",
"pages/todo/todo",
"pages/todo_keyresult/todo_keyresult",
"pages/okr/okr",
"pages/okr_edit/okr_edit",
"pages/okr_create/okr_create",
"pages/okr_detail/okr_detail",
"pages/history/history"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle": "black"
},
"sitemapLocation": "sitemap.json"
}
CRM 销售机会信息管理(三)
3.8 - 线索跟踪
在用户提交了个人信息后,后台产生了线索数据,接下来我们需要对线索进行跟踪和处理。在本任务中,我们需要完成对线索的编辑还有为其添加跟踪记录。在线索的编辑页面,我们通过线索 ID 获取到线索的内容,在页面展示初始化的数据。通过编辑选择用户的意向状态,以及分配给不同的销售。同时在线索列表中,需要展示该分配的销售名称。
新建线索记录 model
/models/log.js1
2
3
4
5
6
7
8
9const Base = require('./base.js');
class ClueLog extends Base {
constructor(props = 'clue_log') {
super(props);
}
}
module.exports = new ClueLog()
在控制器中添加页面渲染、修改、添加记录的方法
/controllers/clue.js1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82// ...
const ClueLog = require('./../models/log.js');
const User = require('./../models/user.js');
const userController = {
...,
log: async function(req,res,next) {
try{
const id = req.params.id;
const clues = await Clue.select({ id })
const logs = await ClueLog.select({ clue_id : id})
const users = await User.select({ role: 2 })
res.locals.users = users.map(data => {
return {
id: data.id,
name: data.name
}
});
res.locals.clue = clues[0]
res.locals.clue.created_time_display = formatTime(res.locals.clue.created_time);
res.locals.logs = logs.map((data)=>{
data.created_time_display = formatTime(data.created_time);
return data
});
res.render('admin/clue_log.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
update: async function(req,res,next) {
let status = req.body.status;
let remark = req.body.remark;
let id = req.params.id;
let user_id = req.body.user_id;
if(!status || !remark){
res.json({ code: 0, message: '缺少必要参数' });
return
}
try{
const clue = await Clue.update( id ,{
status, remark, user_id
});
res.json({
code: 200,
data: clue
})
}catch(e){
console.log(e)
res.json({
code: 0,
message: '内部错误'
})
}
},
addLog: async function(req,res,next){
let content = req.body.content;
let created_time = new Date();
let clue_id = req.params.id;
if(!content){
res.json({ code: 0, message: '缺少必要参数' });
return
}
try{
const clue = await ClueLog.insert({
content, created_time, clue_id
});
res.json({
code: 200,
data: clue
})
}catch(e){
console.log(e)
res.json({
code: 0,
message: '内部错误'
})
}
}
}
添加、修改相关路由
/routes/index.js1
2
3
4// router.get('/admin/clue/:id', function(req, res, next) {
// res.render('admin/clue_log');
// });
router.get('/admin/clue/:id', clueController.log);
routes/api.js1
2
3
4//...
router.put('/clue/:id' , clueController.update);
router.post('/clue/:id/log', clueController.addLog);
//...
修改记录模版,渲染默认数据及创建和引入脚本文件。
/views/admin/clue_log.tpl1
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">跟踪线索</div>
<div class="content-control">
<a href="/admin/clue">返回线索列表 >></a>
</div>
<div class="content-mainer">
<div class="form-section">
<div class="form-item">
<span class="form-text">客户名称:{{clue.name}}</span>
</div>
<div class="form-item">
<span class="form-text">联系电话:{{clue.phone}}</span>
</div>
<div class="form-item">
<span class="form-text">线索来源:{{clue.utm}}</span>
</div>
<div class="form-item">
<span class="form-text">创建时间:{{clue.created_time_display}}</span>
</div>
<div class="form-item">
<span class="form-text">用户状态:</span>
<div class="form-item">
<select id="clueStatus" class="form-input">
<option value="0">请选择线索状态</option>
<option value="1" {% if clue.status == 1 %} selected {% endif %}>没有意向</option>
<option value="2" {% if clue.status == 2 %} selected {% endif %}>意向一般</option>
<option value="3" {% if clue.status == 3 %} selected {% endif %}>意向强烈</option>
<option value="4" {% if clue.status == 4 %} selected {% endif %}>完成销售</option>
<option value="5" {% if clue.status == 5 %} selected {% endif %}>取消销售</option>
</select>
</div>
</div>
<div class="form-item">
<span class="form-text">当前分配销售:</span>
<div class="form-item">
<select id="clueUserId" class="form-input">
<option value="0">请选择分配销售</option>
{% for val in users %}
<option value="{{val.id}}" {% if clue.user_id == val.id %} selected {% endif %}>{{val.name}}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-item">
<p class="form-text">备注:</p>
<textarea id="clueRemark" class="form-textarea" placeholder="备注信息">{{clue.remark}}</textarea>
</div>
<div class="form-item">
<input id="clueId" type="text" hidden value="{{clue.id}}" />
<button id="clueSubmit" class="form-button">保存</button>
</div>
</div>
<div class="log-section">
<ul class="log-list">
{% for val in logs %}
<li class="log-item">
<p class="log-data">{{val.created_time_display}}</p>
<p class="log-content">{{val.content}}</p>
</li>
{% else %}
<li class="log-item">
<p class="log-content">当前没有记录</p>
</li>
{% endfor %}
</ul>
<div class="form-section">
<div class="form-item">
<p class="form-text">添加记录:</p>
<textarea id="logContent" class="form-textarea" placeholder="请输入本次跟踪的记录 ~"></textarea>
</div>
<div class="form-item">
<button id="logSubmit" class="form-button">添加</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/clue_log.js"></script>
{% endblock %}
/public/javascripts/clue_log.js
1 | const PAGE = { |
3.9 - 销售展示
在当前的项目中,我们已经完成了所有数据的流程。用户可以增加和修改,而且可以登录和退出。在落地页中提交的数据在线索管理中可以被编辑、分配和跟踪。但当前我们也发现一个问题,管理员和销售拥有相同的权限,他们可以看相同的页面。因此本任务中,我们需要区分管理员和销售他们所拥有的数据展现,管理员可以操作添加数据,且可以编辑及把线索分配给销售。销售的角色在本项目中,仅可登录且对分配给自己的线索进行展示和跟踪。
切换到管理员身份登录,或者去用户管理页面修改当前账号身份。
使用联表查询,修改线索展示中销售的 id 改为销售名字,并更具返回对应销售角色的数据。
models/clue.js
使用leftjoin方法,否则就会出现当数据库表的某个位置为null时,数据无法加载到页面上1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const Base = require('./base.js');
const knex = require('./knex');
class Clue extends Base {
constructor(props = 'clue') {
super(props);
}
joinUser(params={}){
return knex('clue')
left.join('user', 'clue.user_id', '=', 'user.id')//leftjoin()加不加点也可以用
.select(
'clue.id',
'clue.name',
'clue.phone',
'clue.utm',
'clue.status',
'clue.created_time',
{'sales_name': 'user.name'},
).where(params)
}
}
module.exports = new Clue()
controllers/clue.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const userController = {
...,
show: async function(req,res,next){
try{
//start...
const role = res.locals.userInfo.role;
const user_id = res.locals.userInfo.id;
let params = {};
if (role == 2) {
params.user_id = user_id
}
const clues = await Clue.joinUser(params);
//end...
res.locals.clues = clues.map((data)=>{
data.created_time_display = formatTime(data.created_time);
return data
});
res.render('admin/clue.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
...
}
/views/admin/clue.tpl1
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{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">线索管理</div>
<div class="content-table">
<table class="table-container">
<tr>
<th>姓名</th>
<th>电话</th>
<th>来源</th>
<th>创建时间</th>
<th>跟踪销售</th>
<th>状态</th>
<th>操作</th>
</tr>
{% for val in clues %}
<tr>
<td>{{ val.name }}</td>
<td>{{ val.phone }}</td>
<td>{{ val.utm }}</td>
<td>{{ val.created_time_display }}</td>
<td>{{ val.sales_name }}</td>
{% if val.status == 1 %}
<td>没有意向</td>
{% elif val.status == 2 %}
<td>意向一般</td>
{% elif val.status == 3 %}
<td>意向强烈</td>
{% elif val.status == 4 %}
<td>完成销售</td>
{% elif val.status == 5 %}
<td>取消销售</td>
{% else %}
<td>-</td>
{% endif %}
<td><a href="/admin/clue/{{val.id}}">跟踪</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
在线索记录页面中,对于销售角色隐藏编辑功能。
views/admin/clue_log.tpl1
2
3
4
5
6
7
8
9<!-- ... -->
<div class="content-mainer">
{% if userInfo.role == 1 %}
<div class="form-section">
...
</div>
{% endif %}
</div>
<!-- ... -->
人员管理页面中,仅有管理员可看。如果销售访问,禁止显示。
/views/admin_layout.tpl1
2
3
4
5{% if userInfo.role == 1 %}
<li>
<a class="page-nav-item" href="/admin/user">人员管理</a>
</li>
{% endif %}
/middlewares/auth.tpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const authMiddleware = {
mustLogin: function(req,res,next){
if(!res.locals.isLogin){
res.redirect('/admin/login')
return
}
next();
},
mustRoot: function(req,res,next){
if(res.locals.userInfo.role != 1){
res.writeHead(403);
res.end("403 Forbidden");
return
}
next();
}
}
module.exports = authMiddleware;
再修稿路由即可1
2
3
4
5
6
7router.get('/admin/login', authController.renderLogin);
// router.get('/admin/user',authMiddleware.mustLogin, userController.show);//authMiddleware.mustLogin,
// router.get('/admin/user/create', authMiddleware.mustLogin,userController.renderUserCreate);//
// router.get('/admin/user/:id/edit',authMiddleware.mustLogin, userController.edit);//
router.get('/admin/user', authMiddleware.mustLogin, authMiddleware.mustRoot, userController.show);
router.get('/admin/user/create', authMiddleware.mustLogin, authMiddleware.mustRoot, userController.renderUserCreate);
router.get('/admin/user/:id/edit', authMiddleware.mustLogin, authMiddleware.mustRoot, userController.edit);
把公共样式的登录名修改
view/admin_layou.tpl1
2
3
4
5
6
7<!-- <div class="head-name">林熙</div> -->
{% if userInfo.role == 1 %}
<span class="head-name" value="{{userInfo.role}}">{{userInfo.name}},您好!管理员</span>
{% else %}
<span class="head-name" value="{{userInfo.role}}">{{userInfo.name}},您好,欢迎您</span>
{% endif %}
<a href="/admin/outlogin" id="head-quit">退出</a>
在设置cookie时添加用户名的操作
contlloers/auth.js1
2
3
4···
res.cookie('ac', auth_Code, { maxAge: 24* 60 * 60 * 1000, httpOnly: true });
res.cookie('user_name',user.name,{maxAge:24*60*60*1000,httpOnly:true})//这个位置
···
在中间层添加入名字1
2
3
4
5
6
7
8
9
10
11
12
13const authCodeFunc = require('./../utils/authCode.js');
module.exports = function(req,res,next){
···
let auth_Code = req.cookies.ac;
let name = req.cookies.user_name;//这里
if(auth_Code){
···
res.locals.userInfo = {
phone,password,id,role,name,
}
}
next();
}
完成图
好了,所有步骤已完成
CRM 销售机会信息管理(二)3216346546
3.5 - 用户管理vxzvcfzvdfzsgbvd
在本次用户管理任务中,我们需要连接数据库实现用户的增删改查。我们可以通过用户新建页面提交新建用户并记录到数据库,通过用户列表页面获取数据库中用户的数据并展示出来,在用户编辑页中对匹配的用户内容进行编辑和修改。在本次任务中,我们推荐用 knex.js 完成数据库的交互逻辑,同时我们对这部分的数据库及逻辑操作有 MVC 代码结构的要求。
Knex.js是为 Postgres,MSSQL,MySQL,MariaDB,SQLite3,Oracle 和 Amazon Redshift设计的SQL查询构建器,其设计灵活,便于携带并且使用起来非常有趣。它具有传统的节点样式回调以及用于清洁异步流控制的承诺接口,流接口,全功能查询和模式构建器,事务支持(带保存点),连接池 以及不同查询客户和方言之间的标准化响应。Knex的主要目标环境是Node.js,您需要安装该 knex 库,然后安装适当的数据库库。
任务要求:
安装 Knex.js 连接数据库。
1 | npm install -save knex mysql |
knex中文文档 https://www.songxingguo.com/2018/06/30/knex.js-query/
在根目录中新建 config 文件用来存放数据库的配置信息。
1 | const configs = { |
在根目录中新建 models 文件夹用户存放数据库操作相关文件。
models/knex.js 数据库配置,用于初始化mysql1
2
3
4
5
6
7
8
9
10
11const configs = require('../config');//引入初始化配置信息
module.exports = require('knex')({
client: 'mysql',
connection: {
host: configs.mysql.host,//把configs里的配置信息赋值到host,下同
port: configs.mysql.port,
user: configs.mysql.user,
password: configs.mysql.password,
database: configs.mysql.database,
}
})
新建 models/base.js 基础操作模型(模块化),很多数据库的操作例如简单的增删改查都可以通过继承方法来使用,封装暴露我们自定义的方法,也方便后期 knex 启用的切换不会影响到其他 models 和 controllers 。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const knex = require('./knex');
class Base {
constructor(props) {
this.table = props;
}
all(){//全部
return knex(this.table).select()
}
select(params){//挑选、查找
return knex(this.table).select().where(params)
}
insert(params){//插入
return knex(this.table).insert(params)
}
update(id,params){//修改
return knex(this.table).where('id','=',id).update(params)
}
delete(id){//删除
return knex(this.table).where('id','=',id).del()
}
}
module.exports = Base
新建 models/user.js 用户模型, 继承 base1
2
3
4
5
6
7const Base = require('./base.js');
class User extends Base {
constructor(props = 'user') {
super(props);
}
}
module.exports = new User()
由于数据库存储的日期的数据类型为 Date 类型,因此我们需要新增一个工具函数来进行数据处理,然后在 controllers 中引入进行数据重组。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
module.exports = {
formatTime: formatTime
}
在根目录中新建 controllers 文件夹新建页面逻辑相关的文件。
1 | const User = require('./../models/user.js');//引入数据库模块 |
在根目录中 routes 文件夹中,新建 api.js 文件用户配置 API 相关路由。
1 | var express = require('express'); |
app.js引用api.js文件1
2
3var apiRouter = require('./routes/api');
···
app.use('/api', apiRouter);
在页面路由中,修改用户列表、用户新增、用户修改页面的路由,引用 controller 中的对应的方法。1
2
3
4
router.get('/admin/user', userController.show);
router.get('/admin/user/create', userController.renderUserCreate);
router.get('/admin/user/:id/edit', userController.edit);
在用户新增页面,输入用户姓名、电话、密码、角色提交后数据能存进数据库,成功返回到用户列表页面。
views/admin/user_create.tpl1
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{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">新增人员</div>
<div class="content-control">
<a href="/admin/user">返回用户列表 >></a>
</div>
<div class="form-section">
<div class="form-item">
<input id="userName" type="text" class="form-input" placeholder="姓名"/>
</div>
<div class="form-item">
<input id="userPhone" type="text" class="form-input" placeholder="电话"/>
</div>
<div class="form-item">
<input id="userPassword" type="text" class="form-input" placeholder="密码"/>
</div>
<div class="form-item">
<select class="form-input" id="userRole">
<option value="0">请选择角色</option>
<option value="1">管理员</option>
<option value="2">销售</option>
</select>
</div>
<div class="form-item">
<button id="userSubmit" class="form-button">新增</button>
</div>
</div>
{% endblock %}
{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>//可以自行下载保存也可以使用云端
<script src="/javascripts/user_create.js"></script>
{% endblock %}
public/javascripts/user_create.js1
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
43const PAGE = {
init: function() {
this.bind();
},
bind: function() {
$('#userSubmit').bind('click',this.handleSubmit);
},
handleSubmit: function() {
let name = $('#userName').val();
let phone = $('#userPhone').val();
let password = $('#userPassword').val();
let role = $('#userRole').val();
role = Number(role)
if(!name || !phone || !password || !role){
alert('请输入必要参数啊大锅');
return
}
$.ajax({
url: '/api/user',
data: { name, phone, password, role },
type: 'POST',
beforeSend: function() {
$("#userSubmit").attr("disabled",true);
},
success: function(data) {
if(data.code === 200){
alert('新增成功!')
location.href = '/admin/user'
}else{
alert(data.message)
}
},
error: function(err) {
console.log(err)
},
complete: function() {
$("#userSubmit").attr("disabled",false);
}
})
}
}
PAGE.init();
在用户列表页面,展示数据库中的所有用户信息,并生产对应的编辑地址。
views/admin/user.tpl1
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{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">人员管理</div>
<div class="content-control">
<a href="/admin/user/create">新增人员 >></a>
</div>
<div class="content-table">
<table class="table-container">
<tr>
<th>姓名</th>
<th>电话</th>
<th>角色</th>
<th>创建时间</th>
<th>操作</th>
</tr>
{% for val in users %}
<tr>
<td>{{val.name}}</td>
<td>{{val.phone}}</td>
<td>{{ val.role_display}}</td>
<td>{{ val.created_time_display}}</td>
<td>
<a href="/admin/user/{{val.id}}/edit">编辑</a>
<a class="user-del" href="javascript:;" data-id="{{val.id}}">删除</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/user_page.js"></script>
{% endblock %}
public/javascripts/user_page.js1
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//这里只有删除功能
const PAGE = {
init:function(){
this.bind();
},
bind:function(){
$('.user-del').on('click',this.userDel)
},
userDel: function() {
let id = $(this).data('id');
$.ajax({
url: '/api/user',
data: { id },
type: 'DELETE',
success: function(data) {
if(data.code === 200){
alert('删除成功!')
location.reload()
}else{
console.log(data)
}
},
error: function(err) {
console.log(err)
}
})
},
}
PAGE.init();
在用户编辑页面,可以修改对应的用户相关信息,成功返回到用户列表页面。
views/admin/user_edit.tpl1
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{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">编辑人员</div>
<div class="content-control">
<a href="/admin/user">返回用户列表 >></a>
</div>
<div class="form-section">
<div class="form-item">
<input id="userName" type="text" class="form-input" placeholder="姓名" value="{{user.name}}"/>
</div>
<div class="form-item">
<input id="userPhone" type="text" class="form-input" placeholder="电话" value="{{user.phone}}"/>
</div>
<div class="form-item">
<input id="userPassword" type="text" class="form-input" placeholder="密码" value="{{user.password}}"/>
</div>
<div class="form-item">
<select id="userRole" class="form-input">
<option value="0">请选择角色</option>
<option value="1" {% if user.role == 1 %} selected {% endif %}>管理员</option>
<option value="2" {% if user.role == 2 %} selected {% endif %}>销售</option>
</select>
</div>
<div class="form-item">
<input id="userId" type="text" hidden value="{{user.id}}" />
<button id="userSubmit" class="form-button">保存</button>
</div>
</div>
{% endblock %}
{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/user_edit.js"></script>
{% endblock %}
public/javascripts/user_edit.js1
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
45const PAGE = {
init: function() {
this.bind();
},
bind: function() {
$('#userSubmit').bind('click',this.handleSubmit);
},
handleSubmit: function() {
let id = $('#userId').val();
let name = $('#userName').val();
let phone = $('#userPhone').val();
let password = $('#userPassword').val();
let role = $('#userRole').val();
role = Number(role)
if(!name || !phone || !password || !role){
alert('请输入必要参数');
return
}
$.ajax({
url: '/api/user/' + id,
data: { name, phone, password, role },
type: 'PUT',
beforeSend: function() {
$("#userSubmit").attr("disabled",true);
},
success: function(data) {
if(data.code === 200){
alert('编辑成功!')
location.href = '/admin/user'
}else{
alert(data.message)
}
},
error: function(err) {
console.log(err)
},
complete: function() {
$("#userSubmit").attr("disabled",false);
}
})
}
}
PAGE.init();
登录与退出
在上节任务中,我们实现了用户数据的增删改查,我们用户的数据表中已经拥有了相关的用户。在这次任务中,我们需要在登录页面输入对应的用户手机、密码,如果和数据库中数据匹配校验成功给予登录,否则登录失败。在登录成功后,我们需要通过用户相关的数据加密生成 token 存放在 cookie 中,当用户下一次来的时候如果发现这个 cookie 那么保持用户等登录状态,当用户点击退出的时候,清除对应的 cookie 那么返回到最初始未登录的状态。
登陆页输入手机和密码进行登录。
服务器需要保持用户登录的状态,在下次到登陆页的时候重定向到线索页面。
服务器需要判断用户登录的状态,在未登录情况下去后台相关页面重定向到登录页。
在后台公共头部添加退出按钮,点击退出并重定向到登录页面。
1、在用户登录的 controller 方法中,获取用户提交的电话、密码,到数据库中查询是否有这个用户。如果没有,返回账户密码错误,如果有继续下一步逻辑。
2、把用户的账户、密码、ID 加密成 token ,连同用户名存放在 cookie 中。
controllers/auth.js1
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
42const User = require('./../models/user.js');
const authCodeFunc = require('./../utils/authCode.js');
const authController = {
login:async function(req,res,next){
// 获取邮件密码参数
let phone = req.body.phone;
let password = req.body.password;
// 参数判断
if(!phone || !password){
res.json({ code: 0, data: 'params empty!' });
return
}
try{
// 通过用户模型搜索用户
const users = await User.select({ phone, password });
// 看是否有用户存在
const user = users[0];
// 如果存在
if(user){
// 将其邮箱、密码、id 组合加密
let auth_Code = phone +'\t'+ password +'\t'+ user.id +'\t'+ user.role;
auth_Code = authCodeFunc(auth_Code,'ENCODE');
// 加密防止再 cookie 中,并不让浏览器修改
res.cookie('ac', auth_Code, { maxAge: 24* 60 * 60 * 1000, httpOnly: true });
// 返回登录的信息
res.json({ code: 200, message: '登录成功!'})
}else{
res.json({ code: 0, message: '登录失败,没有此用户!' })
}
}catch(e){
res.json({ code: 0, message: '系统问题请管理员处理' })
}
},
// 渲染登录页面的模版
renderLogin:async function(req,res,next){
res.render('admin/login')
}
}
module.exports = authController;
admin/login.tpl1
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{% extends './../layout.tpl' %}
{% block css %}
<link rel="stylesheet" href="/stylesheets/login.css"/>
{% endblock %}
{% block content %}
<div class="wrapper">
<div class="form-section">
<!-- <div class="form-title">管理系统后台登录</div> -->
<img class="form-title" src="https://www.mercedes-benz.com.cn/content/dam/mb-cn/footer/mercedes-benz-logo-desktop.png">
<div class="form-item">
<input id="userPhone" type="text" class="form-input" placeholder="你的手机"/>
</div>
<div class="form-item">
<input id="userPassword" type="text" class="form-input" placeholder="你的电话"/>
</div>
<div class="form-item">
<button id="userSubmit" class="form-button">内部人员登录</button>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/login.js"></script>
{% endblock %}
login.js1
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
36const PAGE = {
init:function(){
this.bind();
},
bind:function(){
$('#userSubmit').on('click',this.handleSubmit)
},
handleSubmit:function(){
let phone = $('#userPhone').val();
let password = $('#userPassword').val();
$.ajax({
url:'/api/login',
data:{phone,password},
type:'POST',
beforeSend: function(){
$('#userSubmit').attr('disabled',true);
},
success: function(data){
if(data.code === 200){
alert('登陆成功啦');
location.href = '/admin/user'
}else{
alert(data.message)
}
},
error: function(err){
console.log(err)
},
complete: function(){
$('#userSubmit').attr('disabled',false);
},
})
},
}
PAGE.init();
新建一个 middleware的中间层配置,用户判断是否登录,并在路由中引入使用。如果该路由中间件判断用户未登录,即可重定向到登录页面。
filters/initFilter1
2
3
4
5
6
7
8module.exports = function(req,res,next) {
res.locals.seo = {
title:'MercedesBenz CRM',
keywords:'crm',
description:'mercedesbenz-crm'
}
next();
}
filter/loginFilter1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const authCodeFunc = require('./../utils/authCode.js');
module.exports = function(req,res,next){
res.locals.isLogin = false;
res.locals.userInfo = {};
let auth_Code = req.cookies.ac;
if(auth_Code){
auth_Code = authCodeFunc(auth_Code,'DECODE');
authArr = auth_Code.split("\t");
let phone = authArr[0];
let password = authArr[1];
let id = authArr[2];
let role = authArr[3];
res.locals.isLogin = true;
res.locals.userInfo = {
phone,password,id,role
}
}
next();
}
filter/index1
2
3
4module.exports = function(app) {
app.use(require('./initFilter.js'));
app.use(require('./loginFilter.js'))
}
新建中间层
middlewares/suth.js1
2
3
4
5
6
7
8
9
10
11
12
13const authMiddleware = {
mustLogin: function(req,res,next){
if(!res.locals.isLogin){
res.redirect('/admin/login')
return
}
next();
},
}
module.exports = authMiddleware;
在渲染登录页面的 controller 逻辑中,可以获取之前存放在 res.locals 中的数据判断用户是否登录,如果登录了,就重定向到线索管理页面。
1 | // 渲染登录页面的模版 |
router/index
1 | var express = require('express'); |
设置跳转模块,网页要清除cookie才能顺利退出
controllers/outlogin.js1
2
3
4
5
6
7
8const logoutcontroller = {
outlogin:async function(req,res,next){
res.clearCookie('ac','user_name');//清除cookie
res.clearCookie('user_name');
res.redirect('/admin/login');//设置跳转路径
}
}
module.exports = logoutcontroller;
routes/index1
2
3
4
5
6
7···
var logoutcontroller = require('./../controllers/outlogin.js');//引入跳转模块
/* GET home page. */
···
router.get('/admin/outlogin',logoutcontroller.outlogin);//设置路由
module.exports = router;
在公共后台加上退出选项
1 | <div class="wrapper"> |
3.7 - 线索记录
落地页会在各地社交媒体中投放,例如百度推广、朋友圈广告等,拥有意向的用户会填写相关的个人信息进行预约,然后客户将会进行跟踪。本任务我们需要收集落地页中提交的信息,存放到数据库中,并在线索页面中展示。
落地页提交的信息需要包括:用户姓名、手机以及数据来源(在 URL 地址参数中获取 )
线索入库时候,需要记录服务器时间到数据库中(客户端时间会有差异性)。
后台线索列表中,提交时间需要经过格式化处理,如:2019-04-02 13:45。
落地页在获取用户姓名、手机之外还需要在获取 URL中获取来源参数,例如:http://localhost:3000?utm=baidu ,那么需要获取浏览器地址中 utm 的参数值,baidu 。
线索列表中 controller 获取到线索数据,可以 map 一次进行创建时间的 Dateformat。
CRM 销售机会信息管理(一)
0- 项目名称
CRM 销售机会信息管理
1-项目导学页
1.1 - 项目介绍
本项目为奔驰汽车的 CRM 中的销售机会信息管理。奔驰汽车和 4A 广告公司合作做出各类精良的落地页并在各个社交、媒体平台中进行投放。需要一套承接这类落地页收集回来的用户信息,并进行跟踪、统计、反馈的管理系统。通过数据反馈出,哪个落地页有效,哪个渠道获客最多,哪个销售转化最强。
1.2 - 教学目标
本项目为 Node.js 构建 Web 服务框架,主要考察对服务端框架的使用能力,分成以下五个部分: 一、模版生成(View) 二、数据库操作(Model) 三、路由配置( Route) 四、控制器配置( Controller )
1.3 - 前置项目
无
1.4 - 配套资料
设计稿( 仅参考,实现功能就好)
产品流程图 (无)
2-项目剖析页
2.1 - 项目解读
使用 Node.js 的 Express 框架完成用户数据的增删改查、及角色的设置。收集落地页提交的用户信息,管理员进行分配给销售,销售对客户进行跟踪。项目主要包含以下几个模块:
前台
落地页
后台
后台登录
用户管理 1. 用户列表 2. 用户新增 3. 用户修改
线索管理 1. 线索列表 2. 线索跟踪
2.3 - 技能要求
掌握 HTML
掌握 CSS
掌握 Less
掌握 JavaScript
掌握 Axios
了解 Nodejs
了解 MySQL
2.4 - 项目拆解
本项目主要分为 9 个任务:
任务一: 环境搭建 主要通过 express-generator 快速搭建 Web 服务框架。
任务二:数据库设计 主要通过产品原型设计出数据库表结构。
任务三:模版配置 主要通过 Nunjucks 和 router 配置生成各个页面及访问路径。
任务四:页面样式 主要通过 HTML 、CSS 完成所有页面的结构与样式。
任务五:用户管理 主要通过 knex.js 连接 MySQL 实现用户数据的增删改查。
任务六:登录与退出 主要通过 cookie 实现用户登录状态的管理。
任务七:线索记录 主要通过落地页发送的用户数据在线索管理列表中展示。
任务八:线索跟踪 主要通过对应的线索信息进行编辑和提交记录信息。
任务九:销售展示 主要通过权限的区分为管理员与销售展示不同的内容。
任务十:项目优化 主要通过自己的想法,对项目进行调整和修改。
3-任务详情页
3.1 -环境搭建
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,采用 C++ 语言来编写而成。提供了很多系统级的 API,如文件操作、网络处理等,主要用于创建快速的、可扩展的网络应用。 同时,Node.js 的包管理器 npm 是全球最大的开源库生态系统。也正是因为 Nodejs,JavaScript 从仅仅在浏览器运行,到可以在客户端、服务端运行。
安装 Node.js 环境
使用 Node.js 中 Express 框架快速搭建Web 框架,可以在浏览器中展示欢迎页面。
任务提示:
全局安装 express-generator1
npm install -g express-generator
在桌面或其他的地址初始化项目1
2cd ~/Desktop && express MercedesBenz-crm
cd MercedesBenz-crm
下载相关依赖1
npm install
启动项目1
npm start
浏览器打开 http://localhost:3000/ 可以看到 Welcome to Express 啦!1
2
3
4
5
6
7
8
9懒得截图了,浏览器整个页面只有显示以下文字
Express
Welcome to Express
·
扩展知识
目录结构分析
express-generator 帮助我们创建及配置好项目文件,主要有以下:
app.js 主文件
bin/www 启动入口文件
package.json 依赖包管理文件
public 静态资源目录
routes 路由目录
views 模版目录1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.pug
├── index.pug
└── layout.pug
app.js1
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// 各个依赖包
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// 路由文件引用
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
// Express 引用实例化
var app = express();
// 视图模版设置
// 设置视图模版目录,设置视图模版后缀为 jade 文件
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// 使用 morgan 日志打印
app.use(logger('dev'));
// 使用对 Post 来的数据 json 格式化
app.use(express.json());
// 使用对 表单提交的数据 进行格式化
app.use(express.urlencoded({ extended: false }));
// 使用 cookie
app.use(cookieParser());
// 设置静态文件地址路径为 public
app.use(express.static(path.join(__dirname, 'public')));
// 使用配置好的路由
app.use('/', indexRouter);
app.use('/users', usersRouter);
// 捕捉404错误
app.use(function(req, res, next) {
next(createError(404));
});
// 监听异常如果有,立刻返回异常
app.use(function(err, req, res, next) {
// 设置错误信息
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// 渲染到模版
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
3.2 - 数据库设计
项目开发中后端第一个考虑的事情就是数据库的设计,有多少个模块,数据之间怎么关联,需要充分的了解业务的需求,设计出扩展性强的数据结构,以应对之后的需求迭代的功能添加。在本次任务中我们一起来思考,设计一个扩展性强的数据库表结构。
本地安装 MySQL,并使用第三方工具连接
创建项目数据库,并在数据库中创建关联的表
在数据库表中定义好字段以及其属性
在工具中导出 sql 文件
任务提示: 5. 可以使用 XAMPP 集成环境,启动数据库。 6. navicat - 数据库管理工具 - windows 用户安装。 7. Sequel Pro - 数据库管理工具 - macOS 用户安装。 8. 项目的主要模块有:用户、线索、记录。 9. 用户包含的主要字段应该有:姓名、电话、密码、角色。 10. 线索包含的主要字段应该有:姓名、电话、来源、时间。 11. 记录包含的主要字段应该有:内容、时间。
注意: XMAPP 默认用户名为:root , 密码为空 , host 为 127.0.0.1 。请确保 XAMPP 启动,然后使用数据库管理工具连接成功。在 Mac 的新版本XAMPP中,host 未必为 127.0.0.1,可能是 192.168.64.2 ,需要按提示修改文件获取权限,同时设置新的用户和密码。
需求拆解
更具需求我们可以大致了解到主要涉及如下几个方面的内容:
1、用户管理
2、销售线索
3、销售记录
用户管理中主要有 2 个角色,管理人员、销售,管理人员对销售进行增删改查,涉及到的主要信息有:
姓名(name)
电话(phone)
密码(password)
角色(role)[1:管理员,2:销售]
创建时间(created_time)
是否删除(is_deleted)
销售线索主要为落地页提交进来的用户信息,管理员更具销售信息分配给各个销售,销售标记销售线索的状态和备注信息,涉及到的主要信息有:
姓名(name)
电话(phone)
来源(utm)
创建时间(created_time)
是否删除(is_deleted)
销售人员(user_id)
状态(status)[1:没有意向,2:意向一般,3:意向强烈,4:完成销售,5:取消销售]
备注(remark)
销售记录主要为销售更具分配的销售线索进行定期跟踪和记录,记录下每天和用户接触的内容、反馈和必要点,涉及到的主要信息有:
销售线索(clue_id)
创建时间(created_time)
记录内容(content)
因此,更具项目需求我们可以暂定为 3 张表,分别为用户表(user)、线索表(clue)、记录表(clue_log),以其数据结构可以如下所示:
TABLE:user
Field:id
Field:name Type:VARCHAR LENGTH:255 Comment:姓名
Field:phone Type:VARCHAR LENGTH:255 Comment:电话
Field:role Type:INT LENGTH:11 Comment:角色
Field:created_time Type:TIMESTAMP LENGTH:255 Comment:创建时间
Field:is_deleted Type:INT LENGTH:255 Comment:是否删除
TABLE:clue
Field:id
Field:name Type:VARCHAR LENGTH:255 Comment:姓名
Field:phone Type:VARCHAR LENGTH:255 Comment:电话
Field:utm Type:INT LENGTH:11 Comment:角色
Field:user_id Type:INT LENGTH:11 Comment:销售人员
Field:status Type:INT LENGTH:11 Comment:状态[1:没有意向,2:意向一般,3:意向强烈,4:完成销售,5:取消销售]
Field:remark Type:TEXT Comment:备注
Field:created_time Type:TIMESTAMP LENGTH:255 Comment:创建时间
TABLE:clue_log
Field:id
Field:clue_id Type:INT LENGTH:11 Comment:销售线索
Field:content Type:TEXT Comment:内容
Field:created_time Type:TIMESTAMP LENGTH:255 Comment:创建时间
Field:is_deleted Type:INT LENGTH:11 Comment:是否删除
在本地安装 MySQL 数据库以及常用链接工具
XAMPP 集成环境,用于启动数据库
navicat - 数据库管理工具 - windows 用户安装
Sequel Pro - 数据库管理工具 - macOS 用户安装
XAMPP(Apache+MySQL+PHP+PERL)是功能强大的构建网站集成软件包。也是免费的,是使用最多的
在百度输入XAMPP,搜索一下,得到结果,点击进入官网:
在官网,选择和自己系统匹配的软件包,点击下载
启动安装,可以看到安装界面:
一直next,按照自己的需要选择对应的参数和路径等等,等待安装完成:
安装完成后,启动control panel,可以启动想要的服务:
因为我的电脑是window 10系统,所以安装navicat
进入navicat官网,选择Navicat for MySQL,然后点击进行下载即可
弹出下图界面,任意选择一个和电脑位数对应的版本即可。
修改安装位置,一般选择安装在非系统盘
完成安装
启动数据库
使用工具连接 MySQL
右键点击root连接
新建数据库仓库 Databse 设置编码格式
Database Name : MercedesBenz-crm
Database Encoding : UTF-8 Unicode(utf8mb4)
Databae Collation : Default(utf8mb4_general_ci)
创建数据库表 Table 并设置其字段和熟悉
3.3 - 模版配置
模板引擎,这里特指用于Web开发的模板引擎。是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。通过模版引擎,我们可以在页面结构中使用语法进行判断、循环以及数据的嵌套操作。在本次项目中,我们使用 express-generator 快速生产框架时候自带 Pug 模版,我们需要把它换成 nunjucks,因为它的语法更接近于 HTML ,同时功能丰富、可扩展性强。
1、替换模版引擎为 nunjucks。
安装 nunjucks 依赖1
npm install --save nunjucks
2、参照使用文档修改配置信息1
2
3
4
5
6
7
8
9
10
11
12var nunjucks = require('nunjucks');// 引入 nunjucks
var app = express();
// view engine setup
// app.set('views', path.join(__dirname, 'views'));注释原有的jde模板
// app.set('view engine', 'jade');
app.set('view engine', 'tpl');替换成tpl模板
nunjucks.configure('views', {
autoescape: true,
express: app,
watch: true
});
3、删除 views 目录中的原有的 jade 模版文件
4、在 views 目录中添加 layout.tpl、index.tpl、error.tpl文件
layout.tpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
{% block css %}
{% endblock %}
</head>
<body>
{% block content %}
{% endblock %}
{% block js %}
{% endblock %}
</body>
</html>
error.tpl1
2
3
4
5
6
7{% extends './layout.tpl' %}
{% block content %}
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
{% endblock %}
index.tpl1
2
3
4
5{% extends './layout.tpl' %}
{% block content %}
<p>落地页</p>
{% endblock %}
5、在 views 文件夹目录新建 admin 文件夹,用来存放登录页面与后台相关页面的模版
登陆页、用户管理 – 用户列表页、用户管理 – 创建用户页、用户管理 – 编辑用户页、线索管理 – 线索列表页、线索管理 – 线索记录页,七个页面以此类推
/views/admin/login.tpl1
2
3
4
5{% extends './../layout.tpl' %}
{% block content %}
<p>登陆页</p>
{% endblock %}
6、配置页面路由
/routes/index.js1
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
33var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index');
});
router.get('/admin/login', function(req, res, next) {
res.render('admin/login');
});
router.get('/admin/user', function(req, res, next) {
res.render('admin/user');
});
router.get('/admin/user/create', function(req, res, next) {
res.render('admin/user_create');
});
router.get('/admin/user/:id/edit', function(req, res, next) {
res.render('admin/user_edit');
});
router.get('/admin/clue', function(req, res, next) {
res.render('admin/clue');
});
router.get('/admin/clue/:id', function(req, res, next) {
res.render('admin/clue_log');
});
module.exports = router;
7、跑起来,测试各个页面的情况1
npm start
http://localhost:3000/
http://localhost:3000/admin/login
http://localhost:3000/admin/user
http://localhost:3000/admin/user/create
http://localhost:3000/admin/user/1/edit
http://localhost:3000/admin/clue
http://localhost:3000/admin/clue/1
8、安装全局依赖包 nodemon
向。然后在终端中可以直接使用该包提供的服务,安装全局依赖包多数为工具类型的包。
以下我们安装 nodemon ,为开发常用监听工具,这样我们就不需要修改了服务代码之后每次都自己手动重启,在代码发生修改后,nodemon会自动帮助我们重启服务1
npm install -g nodemon
9、使用 nodemon 重启服务1
nodemon ./bin/www
然后我们修改页面模版也会得到相应的相应,不需要多次手动重启服务。
3.4 - 页面样式
在上一节中我们配置好了各个页面的模版文件,这次我们需要为其添加 HTML 相关的 DOM 结构以及使用 CSS 丰富其展示效果。在本次任务中,我们需要为 7 个页面添加其结构和样式,大家可以尽情发挥。
登录页必须包含内容:电话、密码的表单组合。
落地页必须包含内容:项目、电话的表单组合。
用户列表页必须包含内容有:导航、用户信息相关表格。
用户新建页必须包含内容有:导航、用户名称、电话、密码、角色的表单组合。
用户编辑页必须包含内容有:导航、用户名称、电话、密码、角色的表单组合。
线索列表页必须包含内容有:导航、线索信息相关表格。
线索记录页必须包含内容有:导航、线索名称、电话、意向程度、所属销售组合的表单,以及时间和内容的记录流程。
任务提示:
后台各个页面中涉及到的表单、表单项、表格的样式可以进行复用。
后台的样式框架如导航、公共头、公共尾部的结构和样式可以抽离。
对于 BootStrap 熟悉的同学可以使用 BootStrap 或者 Martin UI 库。
实现步骤
落地页结构与样式,可参考如下,主要结构为用户姓名与电话的提交表单。
/views/index.tpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23{% extends './layout.tpl' %}
{% block css %}
<link rel="stylesheet" href="/stylesheets/index.css"/>
{% endblock %}
{% block content %}
<div class="wrapper">
<div class="header"><img class="head-logo" src="https://www.mercedes-benz.com.cn/content/dam/mb-cn/footer/mercedes-benz-logo-desktop.png"></div>
<div class="form-section">
<h2 class="form-title">留下电话,我们会马上联系你,为你预约优惠名额</h2>
<div class="form-item">
<input type="text" class="form-input" placeholder="你的姓名"/>
</div>
<div class="form-item">
<input type="text" class="form-input" placeholder="你的电话"/>
</div>
<div class="form-item">
<button class="form-button">马上抢占名额</button>
</div>
</div>
</div>
{% endblock %}
/public/stylesheets/index.css1
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
67
68html,body{
width: 100%;
height: 100%;
}
*{
margin:0;
padding:0;
box-sizing: border-box;
}
.wrapper{
width: 100%;
height: 100%;
background-image: url(https://www.mercedes-benz.com.cn/content/dam/mb-cn/vehicles/suv/gle-suv/gle-suv-fuben/gle-suv-mob0416.jpg);
background-position: center;
}
.header{
width: 100%;
height: 120px;
background-color: #222;
}
.head-logo{
width: 259px;
height: 64px;
margin-top: 28px;
margin-left:30px;
}
.form-section{
position: fixed;
left: 50%;
top: 36%;
transform: translate(-50%,-36%);
padding: 80px;
border-top: 1px solid #e4e4e4;
background-color: rgba(255,255,255,.6);
}
.form-title{
text-align: center;
font-size: 18px;
color: #4f4f4f;
}
.form-item{
width: 400px;
margin: 20px auto;
}
.form-input{
display: block;
width: 100%;
height: 40px;
line-height: 40px;
padding: 10px;
border: 1px solid #e4e4e4;
border-radius: 4px;
font-size: 14px;
outline: none;
background-color: #fcfcfc;
}
.form-button{
display: block;
width: 100%;
height: 40px;
text-align: center;
font-size: 16px;
background-color: #00ADEF;
color: #fff;
outline: none;
border:none;
cursor: pointer;
}
2、设置用户登录页的结构样式
/views/admin/login.tpl1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22{% extends './../layout.tpl' %}
{% block css %}
<link rel="stylesheet" href="/stylesheets/login.css"/>
{% endblock %}
{% block content %}
<div class="wrapper">
<div class="form-section">
<div class="form-title">管理系统后台登录</div>
<div class="form-item">
<input type="text" class="form-input" placeholder="你的姓名"/>
</div>
<div class="form-item">
<input type="text" class="form-input" placeholder="你的电话"/>
</div>
<div class="form-item">
<button class="form-button">马上抢占名额</button>
</div>
</div>
</div>
{% endblock %}
/public/stylesheets/login.css1
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
60html,body{
width: 100%;
height: 100%;
}
*{
margin:0;
padding:0;
box-sizing: border-box;
}
.wrapper{
width: 100%;
height: 100%;
background-image: url(https://www.mercedes-benz.com.cn/content/dam/mb-cn/flexperience/bannerpc_0410.jpg);
background-position: center;
}
.form-section{
position: fixed;
top: 36%;
left: 50%;
transform: translate(-50%,-36%);
padding: 40px;
border: 1px solid #e5e5e5;
width: 450px;
border-radius: 4px;
background-color: rgba(255,255,255,.3);
box-shadow: 2px 2px 56px rgba(0,0,0,.13);
}
.form-title{
color: #666;
font-size: 30px;
text-align: center;
margin-bottom: 40px;
}
.form-item{
margin: 20px auto;
}
.form-input{
display: block;
width: 100%;
height: 40px;
line-height: 20px;
padding: 10px;
border: 1px solid #e5e5e5;
border-radius: 4px;
background-color: #fff;
font-size: 14px;
outline: none;
}
.form-button{
display: block;
width: 100%;
height: 40px;
text-align: center;
font-size: 16px;
background-color: #00ADEF;
color: #fff;
border:none;
outline: none;
cursor: pointer;
}
3、后台公共模版
由于后台展示的头部、导航布局类似,并且都是用同一样式的组建,因此我们可以抽离出公共的后台 layout 模版。
新建:/views/admin_layout.tpl1
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<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/stylesheets/admin.css"/>
{% block css %}
{% endblock %}
</head>
<body>
<div class="wrapper">
<header class="page-header">
<img class="page-header-img" src="https://www.mercedes-benz.com.cn/content/dam/mb-cn/footer/mercedes-benz-logo-desktop.png">
<div class="head-name">林熙</div>
</header>
<div class="page-body">
<div class="page-aside">
<nav class="page-nav">
<ul>
<li>
<a class="page-nav-item" href="/admin/user">人员管理</a>
</li>
<li>
<a class="page-nav-item" href="/admin/clue">线索管理</a>
</li>
</ul>
</nav>
</div>
<div class="page-content">
{% block content %}
{% endblock %}
</div>
</div>
<footer class="page-footer">Copyright © 2019 极客学院体验技术部出品</footer>
</div>
{% block js %}
{% endblock %}
</body>
</html>
4、修改用户管理[用户列表页]模版
/views/user.tpl1
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{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">人员管理</div>
<div class="content-control">
<a href="/admin/user/create">新增人员 >></a>
</div>
<div class="content-table">
<table class="table-container">
<tr>
<th>姓名</th>
<th>电话</th>
<th>角色</th>
<th>创建时间</th>
<th>操作</th>
</trs>
<tr>
<td>周杰伦</td>
<td>13511111111</td>
<td>管理员</td>
<td>2019-4-1</td>
<td><a href="/admin/user/1/edit">编辑</a></td>
</tr>
<tr>
<td>李易峰</td>
<td>13522222222</td>
<td>管理员</td>
<td>2019-4-1</td>
<td><a href="/admin/user/2/edit">编辑</a></td>
</tr>
<tr>
<td>陈奕迅</td>
<td>13533333333</td>
<td>管理员</td>
<td>2019-4-1</td>
<td><a href="/admin/user/3/edit">编辑</a></td>
</tr>
</table>
</div>
{% endblock %}
5、修改用户管理[创建用户页]模版
/views/user_creat.tpl
1 | {% extends './../admin_layout.tpl' %} |
6、修改用户管理[编辑用户页]模版
/views/user_edit.tpl1
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{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">编辑人员</div>
<div class="content-control">
<a href="/admin/user">返回用户列表 >></a>
</div>
<div class="form-section">
<div class="form-item">
<input type="text" class="form-input" placeholder="姓名"/>
</div>
<div class="form-item">
<input type="text" class="form-input" placeholder="电话"/>
</div>
<div class="form-item">
<input type="text" class="form-input" placeholder="密码"/>
</div>
<div class="form-item">
<select class="form-input">
<option value="0">请选择角色</option>
<option value="0">管理员</option>
<option value="0">销售</option>
</select>
</div>
<div class="form-item">
<button class="form-button">保存</button>
</div>
</div>
{% endblock %}
7、修改线索管理[线索列表页]模版
/views/clue.tpl1
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{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">线索管理</div>
<div class="content-table">
<table class="table-container">
<tr>
<th>姓名</th>
<th>电话</th>
<th>来源</th>
<th>创建时间</th>
<th>跟踪销售</th>
<th>状态</th>
<th>操作</th>
</trs>
<tr>
<td>周杰伦</td>
<td>13511111111</td>
<td>baidu_search</td>
<td>2019-4-1</td>
<td>陈奕迅</td>
<td>意向强烈</td>
<td><a href="/admin/clue/1">跟踪</a></td>
</tr>
<tr>
<td>李易峰</td>
<td>13522222222</td>
<td>baidu_search</td>
<td>2019-4-1</td>
<td>陈奕迅</td>
<td>意向强烈</td>
<td><a href="/admin/clue/1">跟踪</a></td>
</tr>
<tr>
<td>陈奕迅</td>
<td>13533333333</td>
<td>baidu_search</td>
<td>2019-4-1</td>
<td>陈奕迅</td>
<td>意向强烈</td>
<td><a href="/admin/clue/1">跟踪</a></td>
</tr>
</table>
</div>
{% endblock %}
8、修改线索管理[线索记录页]模版
/views/clue_log.tpl1
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
67
68{% extends './../admin_layout.tpl' %}
{% block content %}
<div class="content-title">跟踪线索</div>
<div class="content-control">
<a href="/admin/clue">返回线索列表 >></a>
</div>
<div class="content-mainer">
<div class="form-section">
<div class="form-item">
<span class="form-text">客户名称:周秀娜</span>
</div>
<div class="form-item">
<span class="form-text">联系电话:1351231232</span>
</div>
<div class="form-item">
<span class="form-text">线索来源:baidu_search</span>
</div>
<div class="form-item">
<span class="form-text">创建时间:2019-4-1</span>
</div>
<div class="form-item">
<span class="form-text">线索状态:意向强烈</span>
</div>
<div class="form-item">
<span class="form-text">用户状态:</span>
<div class="form-item">
<select class="form-input">
<option value="0">请选择线索状态</option>
<option value="1">没有意向</option>
<option value="2">意向一般</option>
<option value="3">意向强烈</option>
<option value="4">完成销售</option>
<option value="5">取消销售</option>
</select>
</div>
</div>
<div class="form-item">
<p class="form-text">备注:</p>
<textarea class="form-textarea" placeholder="备注信息"></textarea>
</div>
<div class="form-item">
<button class="form-button">保存</button>
</div>
</div>
<div class="log-section" style="position: relative;">
<ul class="log-list">
<li class="log-item">
<p class="log-data">2019-4-1</p>
<p class="log-content">第一次打电话过去,用户立刻挂掉,再接再厉</p>
</li>
<li class="log-item">
<p class="log-data">2019-4-1</p>
<p class="log-content">第一次打电话过去,用户立刻挂掉,再接再厉</p>
</li>
</ul>
<div class="form-section" style="position: absolute;bottom: 0;">
<div class="form-item">
<p class="form-text">添加记录:</p>
<textarea class="form-textarea" placeholder="请输入本次跟踪的记录 ~"></textarea>
</div>
<div class="form-item">
<button class="form-button">添加</button>
</div>
</div>
</div>
</div>
{% endblock %}
9、设置后台样式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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174html,body{
width: 100%;
height: 100%;
}
*{
margin:0;
padding:0;
box-sizing: border-box;
}
a{
text-decoration: none;
}
/*布局与导航*/
.wrapper{
height: 100vh;
display: flex;
flex-direction: column;
}
.page-header{
position: relative;
height: 110px;
text-align: right;
font-size: 16px;
background-color: #222;
color: #fff;
}
.head-name{
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
cursor: pointer;
}
.page-header-img{
position: absolute;
left: 30px;
top: 26px;
width: 220px;
height: 54px;
}
.page-body{
flex: 1;
border-top: 1px solid #e4e4e4;
border-bottom: 1px solid #e4e4e4;
}
.page-body{
display: flex;
}
.page-aside{
width: 240px;
border-right: 1px solid #e4e4e4;
background-color: #222;
}
.page-content{
flex: 1;
padding: 0 40px;
background-color: #f5f5f5;
}
.page-footer{
height: 50px;
text-align: center;
font-size: 15px;
line-height: 50px;
color: #fff;
background-color: #222;
}
.page-nav-item{
display: block;
height: 60px;
padding: 10px 20px;
line-height: 40px;
font-size: 14px;
color: #999;
text-decoration: none;
}
.page-nav-item.active{
background: #666;
color: #f5f5f5;
}
/*操作控制容器*/
.content-title{
height: 60px;
padding: 10px 0;
line-height: 40px;
font-size: 16px;
}
.content-control{
margin: 20px 0;
color: #333
}
.content-control a{
color: #333;
}
/*表格样式*/
.table-container{
border-collapse: collapse;
margin: 1rem 0;
width: 100%;
}
.table-container a{
color: #333;
}
.table-container tr{
border-top: 1px solid #dfe2e5;
}
.table-container tr:nth-child(2n) {
background-color: #f6f8fa;
}
.table-container th{
font-style: 16px;
font-weight: 600;
}
.table-container td,
.table-container th{
border: 1px solid #dfe2e5;
padding: .6em 1em;
}
/*表单样式*/
.form-item{
width: 400px;
margin: 20px 0;
outline: none;
}
.form-input{
display: block;
width: 100%;
height: 40px;
line-height: 40px;
padding: 10px;
border: 1px solid #e4e4e4;
border-radius: 4px;
font-size: 14px;
}
.form-textarea{
display: block;
width: 100%;
line-height: 20px;
height: 80px;
padding: 10px;
border: 1px solid #e4e4e4;
border-radius: 4px;
font-size: 14px;
}
.form-button{
display: block;
width: 100%;
height: 40px;
text-align: center;
font-size: 16px;
background-color: #222;
color: #f5f5f5;
border:none;
outline: none;
cursor: pointer;
}
/*记录列表样式*/
.content-mainer{
display: flex;
}
.log-section{
flex: 1;
margin-left: 60px;
}
.log-item{
margin: 20px 0;
}
.log-item .log-date{
font-size: 16px;
}
.log-item .log-content{
font-size: 14px;
color: #666;
}
微信小程序微入门
第一步 你得申请个账号
在浏览器中输入 https://mp.weixin.qq.com 微信公众平台官网地址,点击右上角的立即注册,接着选择小程序。填写你的邮箱、和密码。点击注册,并按步骤进行邮箱的激活和信息校验即可。
第二步 下载微信开发者工具,把它安装到桌面上,
1、在桌面新建名为 go-kart 文件夹
2、在开发者工具中,选择小程序项目(左边)
选择桌面 go-kart 文件夹,添加微信小程序后台中的 AppID,设置项目名称,选择建立普通快速启动模版
基于 Vue 实现 todoApp
1-项目导学页
1.1 - 项目介绍
todoApp 能够合理让员工规划工作日程,让管理者及时掌握员工工作饱和度、工作进展状况等等。这样不管是个人高效完成工作,还是团队协同作业,都可以轻松搞定。日事清的核心功能是日程管理、任务协作和工作笔记,三者有机结合互为一体,让工作体验变得轻松。
1.2 - 教学目标
本项目为 Vue全家桶实现日事清,主要为使用 Vue 及其生态构建 TodoApp。主要考察对 Vue 及其相关生态在项目中的应用能力,分成以下两个部分:
Vue-CLI3
Vue-router
Vuex
1.3 - 前置技能
掌握 HTML
掌握 CSS
掌握 Less
掌握 JavaScript
了解 Vue
了解 Vue-router
了解 Vuex
效果图
2-项目剖析页
2.1 - 项目解读
本项目为 Vue全家桶实现日事清,主要为使用 Vue 及其生态构建 TodoApp。项目主要包含以下几个模块:
输入框
列表
状态导航
2.3 - 技能要求
掌握 HTML
掌握 CSS
掌握 Less
掌握 JavaScript
了解 Vue
了解 Vue-router
了解 Vuex
2.4 - 项目拆解
本项目主要分为 12 个任务:
任务一: 环境搭建 主要通过 Vue-CLI 3 快速搭建 Vue 开发环境,清空默认的结构与样式。并参照 todomvc.com 在 App.vue 中完成基础结构与样式引用。
任务二:列表渲染 主要通过在 Vue data 属性中定义 todos 列表的默认数据 todos 数组,然后使用 for 循环语法,把数据渲染到页面上。
任务三:列表修改 通过 v-model 双向绑定 todo 数据与 input 元素的关系,通过改变 input 内容实现动态修改。少侠不妨再思考一下 Vue监听文本框数据变化有几种方式? 不知道也没关系,内事问百度外事问谷歌。
任务四:状态切换 todolist 中有 完成和未完成两个状态,想看哪里点哪里。
任务五:添加项目 监听回车事件,获取当前输入值往 todos[Array] 数据中 push 新数据实现数据的添加。
任务六:删除项目 通过监听删除事件,获取当前删除元素匹配在 todos 的位置使用数组 splice 方法删除数据。
任务七:项目筛选 通过定义筛选条件,根据条件的不同 filter 返回不同的展示数据。
任务八:组件分离 通过组件的分离,使结构更有层级性及复用性。
任务九:添加路由 主要通过 Vue-router 实现路由的切换,不同的路由地址匹配不同的筛选条件展示不同的数据。
任务十:全局数据 主要通过 Vuex 实现全局数据管理,在跨组件的数据通讯中显得更加的便利。
3-任务详情页
3.1 -环境搭建
Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。
在本任务中,我们使用 Vue CLI3 构建 Vue 开发文件,同时参照 todomvc.com 中的实现效果,在完成 App.vue 中完成 HTML 及 CSS 的基础建设。
任务要求:
1、使用 Vue CLI 来搭建 Vue 开发环境。
2、完成 Todos 的结构与样式
任务提示:
下载安装 Vue-CLI 3,使用 vue create
Vue-CLI 3参考地址 https://segmentfault.com/a/1190000014627083
3.2- 列表渲染
在上一任务中,我们已经完成了基础结构的建设,在本次任务中,我们需要把默认数据渲染到页面中。在 Vue.data 中定义 todos 存储列表数据,每一项中包含 title、compeleted 字段来表示名称与状态。在 templete 模版中通过 for 语句渲染 todos 数据,再在 class 中匹配项目中的 completed 属性,如果项目中 completed 为 true 展示已完成状态。
任务要求:
1、在 Vue.data 中定义 todos,并在页面中绑定与展示。
2、todos 项目中,需要有完成和未完成状态。
任务提示:
1、在 Vue.data 中定义 todos,todo 包含 title、completed
2、在 templete 中使用 for 绑定 todos 数据进行渲染
1 | <ul class="todo-list"> |
给li 绑定一个v-for对象;绑定V-bind(:为简写)class属性,用于切换CSS样式
1 | export default { |
效果图
css样式还没做好。。。
3.3- 列表修改
在 todos List 的样式状态,除了未完成、已完成,还有一个编辑中的状态,双击 todo 项目,切换到编辑状态,对 todo 中 input 进行聚焦,在失去焦点时候恢复为原来对应的状态。在本次任务中,我们一起来完成列表的修改。
任务要求:
1、双击 todo 内容,其下面的 input 聚焦
2、对 todo 的 input 修改后失去焦点或者按回车键,返回原来 todo 状态并展示修改后的内容
任务提示:
为 todo 中 label 元素绑定 dbclick 事件,当点击 todo 时候把 todo 存储到 Vue.data.editTodo 属性中,同时把当前的 title 存储到 Vue.data.beforeEditCache 作为缓存,方便撤销操作。
在 todos 渲染中判断,如果 editTodo 等于该 todo 则显示编辑状态并且其 input 聚焦
1、为 input 元素于 todo.title 进行双向绑定,当修改 input 时同时修改 title 属性
2、为 input 绑定键盘事件与失去焦点事件,当时机出发时设置 editTodo 为 Null 返回原来状态。
3、为 input 绑定 ese 取消事件,取 beforeEditCache 的值重设。
1 | <li v-for="(item,index) in show" :class="[item.completed && 'completed',item.editing && 'editing']"> |
1 | methods:{//数据 |
3.4-状态切换
当前 todo中有 完成和未完成两个状态,当我们点击项目状态选项时候,反选其状态,同时在顶部输入框左侧的全选按钮,点击选项及点击全反选。在本次任务中,我们需要完成 Todos 的完成与未完成的状态切换。
任务要求:
1、点击完成的 todo 左侧状态按钮,切换为未完成
2、点击未完成的 todo 左侧状态按钮,切换未完成
3、单击顶部全选按钮,如果未全选切换所有 todo 为全选
4、点击顶部全选按钮,如果全选切换所有 todo 为未全选
任务提示:
1、为 checkbox 双向绑定 todo.completed 属性
2、为 Vue.computed 添加 allDone ,其 get 读取属性返回当前所有 todos 的 completed 是否为 true,其 set 设置所有 todos 的 completed 值为当前 get 值的反选择。
双向绑定 todo.completed 属性
1 | <input class="toggle-all" id="toggle-all" type="checkbox" :checked="chooseAll"> |
1 | <input class="toggle" type="checkbox" :checked="item.completed" @click="toggleCompleted(index)"> |
1 | toggleCompleted(index){ |
3.5- 添加项目
本任务中,我们为列表添加多一项,当用户在顶部输入框输入时,当按钮下回车键时候,为列表中添加当前项目,title 为输入值,状态为未完成。
任务要求:
1、在输入框中输入完毕,按回车键,往列表中添加未完成的一项
任务提示:
1、为顶部输入框绑定监听回车事件,事件触发时候为 todos 数据中 push 一项,同时讲当前 value 设置为空。
1 | <input class="new-todo" placeholder="What are you 弄啥咧?" autofocus @keyup.enter="create" v-model="value">//绑定键盘回车事件 |
1 | create(){ |
3.6- 删除项目
本任务中,我们需要来完成删除项目的功能。删除场景主要有 2 个,一个为在 todo hover 之后的右侧有一个关闭按钮,当点击关闭按钮时候删除当前 todo 项目。在脚步导航的右侧有一个删除所有已完成的按钮,点击删除所有已完成的 todo 项目。
任务要求:
1、完成单条 todo 删除功能
2、完成所有已完成的 todo 删除功能
任务提示:
1、为删除按钮绑定点击事件,点击在 todos 移除当前自己的项目
2、为删除所有按钮绑定点击事件,点击 todos.filter 一下 todo 重新设置 todos
给button绑定一个点击事件
1 | <button class="destroy" @click="destroy(index)"></button> |
利用splice删除添加删除项目和数量
1 | destroy(index){ |
获取todos的内容,过滤掉completed为false的内容
1 | clearCompleted(){ |
3.7- 项目筛选
在底部导航位置有三个状态,分别为 all、active、completed,全部、进行中、已完成的意思,在本次任务中,我们需要筛选切换不同的状态来动态展示对应状态的数据。
任务要求:
1、默认在 all 状态,展示所有状态 todo 。
2、点击 active 状态,展示没有完成的 todo 。
3、点击 completed 状态,展示已完成的 todo 。
任务提示:
1、定义 Vue.data.filter 状态为 all
2、定义展示数据 showTodo ,更具 filter 的类型返回不同的数据
3、点击导航切换 filter 的值
给li绑定一个v-for,数据来自filters;给a标签绑定点击事件,把key参数传进点击事件,显示层利用数据驱动,有多少就渲染就多少
1 | <li v-for="(item,key) in filters"> |
把data里的参数换成key的参数
1 | changeFilter (key) { |
效果图
3.8 - 组件分离
在以上任务中,我们已经完成了 Todos 的增删改查大逻辑的功能,在接下来这个任务中,我们需要对我们的代码进行组件分离。目前我们的代码全部都写在 App.vue 上面,接下来我们需要对其进行拆分,例如拆分为 TheHeader 头部输入框、TodeList 展示列表、以及 TheFooter 尾部,然后再组合应用在 App.vue 中。
任务要求:
1、把页面拆分为 TheHeader、TodoList、TheFooter 三个部分
2、分离后能正常使用
任务提示:
在1、 compmoents 目录中新建对应的五个部分文件1
2
3
4
5components/TheHeader.vue
components/TheList.vue
components/TheFooter.vue
App.vue
assets/index.css
组件分离
App.vue
1 | <template> |
TheHeader.vue
1 | <template> |
TheList.vue
1 | <template> |
TheFooter.vue
1 | <template> |
3.9- 添加路由
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。
任务要求:
1、下载并使用 vue-router ,开启 history 模式
2、匹配地址到指定的组件中
3、当点击切换导航时候,URL 地址发送变化,列表数据也发生变化
4、当在某路由原地刷新,列表数据保持和路由对应的状态不变
任务提示:
1、下载 vue-router,并在 main.js 中引入vue.use 路由
2、路由配置 history 及 routes 属性
3、在 new Vue 中传入路由
4、在 App.vue 中配置 router-view
5、在底部导航配置 router-link 抽离容器组件 TheTodes 使用 watch 监听 $router 的变化,在触发的回调函数中获取 params 的值,修改 filter 数据。同时在页面 created 生命周期中也调用此回调函数。
下载vue-router
1 | npm install vue-router |
参考地址https://router.vuejs.org/zh/installation.html
在router.js中配置路由
1 | import Vue from 'vue' |
在 new Vue 中传入路由
1 | import Vue from 'vue' |
在 App.vue 中配置 router-view
1 | <template> |
内容都转移到views/Todos.vue
在底部导航配置 router-link
####在底部导航配置 router-link 抽离容器组件 TheTodes 使用 watch 监听 $router 的变化,在触发的回调函数中获取 params 的值,修改 filter 数据。同时在页面 created 生命周期中也调用此回调函数。1
2
3<li v-for="(item,key) in filters">
<router-link :class="[filter === key && 'selected']" :to="'/' + key">{{item}}</router-link>
</li>
在底部导航配置 router-link 抽离容器组件 TheTodes 使用 watch 监听 $router 的变化,在触发的回调函数中获取 params 的值,修改 filter 数据。同时在页面 created 生命周期中也调用此回调函数。
1 | computed:{ |
####
####
####
Vuex
使用 Vuex 来完成我们的数据全局交互
下载vuex
1 | npm install --save vuex |
在根目录新建一个文件夹,创建 store.js文件,编辑公共内容,然后在main.js里引用
1 | import Vue from 'vue' |
然后在 main.js引用
1 | mport Vue from 'vue' |
通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到
修改Todos.vue的内容
1 | <template> |
把组件内的内容用stort连接
TheHeader.vue
1 | <template> |
TheList.vue
1 | <template> |
TheFooter.vue
1 | <template> |
大功告成,无Bug无报错
Vue-router
使用 watch 监听 $router 的变化
在根目录里main.js里添加路由
1 | import router from '@/router/index' |
同时在new Vue里加上变量名
新建router文件夹/index.js,加下以上内容
新建views/Todos.Vue文件,同时把app.vue的内容迁移到odos.Vue,app.vue里只配置
添加路由
在底部导航配置 router-link 抽离容器组件 TheTodes 使用 watch 监听 $router 的变化,在触发的回调函数中获取 params 的值,修改 filter 数据。同时在页面 created 生命周期中也调用此回调函数。
对底部导航进行路由配置,v-for循环filters列表,当filter===key值时,去识别当前的filter,由此去切换路由。
:class绑定
我们可以向 v-bind:class 传入一个对象,来动态地切换 class
在Todos.Vue里使用 watch 监听 $router 的变化
当路由id为filter=”all”时,会切换回all状态,展示所有状态todos
当路由id为filter=”active”时,点击 active 状态,展示没有完成的todos
当路由id为filter=”completed”时,点击completed状态,展示已完成的 todos
当在某路由原地刷新,列表数据保持和路由对应的状态不变,通过created来实现
Hello World
如何使用HEXO + Gitub搭建一个简单的个人博客
什么是 Hexo?
Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。
相关步骤:
1、安装Node.js和配置好Node.js环境
1 | $ npm install -g hexo-cli |