# C-vue-todo
**Repository Path**: carlos-sam/c-vue-todo
## Basic Information
- **Project Name**: C-vue-todo
- **Description**: 我的第一个项目仓库,欢迎大家来串门。O(∩_∩)O哈哈~
- **Primary Language**: Unknown
- **License**: WTFPL
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 16
- **Forks**: 0
- **Created**: 2022-10-18
- **Last Updated**: 2025-03-03
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 一、需求分析
## (一)系统框架

## (二)具体的业务需求
### 1.展示项目
1. 1. 有一个项目列表,可以展示用户添加到数据库中的所有项目
2. 项目列表中,每个项目子项从上到下依次排列
3. 每个子项中都会显示项目内容
### 2.添加项目
1. 1. 输入框中输入添加的项目内容
2. 按回车添加项目
3. 如果输入框中没有内容则提示‘内容不能为空’
4. 如果输入框中有内容则添加到项目列表的最上方
5. 用户进入页面后鼠标会自动聚焦到输入框中
6. 输入框中有提示文字
7. 输入完成后会自动清空输入框中的内容
### 3.勾选项目
1. 1. 每个项目都有一个勾选按钮
2. 点击按钮后项目处于完成状态
3. 当项目处于完成状态时勾选按钮高亮
4. 处于完成状态的项目内容将会被加上删除线
### 4.修改项目
1. 1. 双击项目内容,会出现一个输入框
2. 在输入框中可以修改项目内容
3. 输入框出现时鼠标会自动聚焦
4. 输入框中会回显原来的项目内容
5. 修改完成,当用户按下Enter(回车键)或者失去焦点时,更新项目内容
6. 当用户按下Esc键时,会取消修改
7. 如果完成修改时,输入框中的内容为空,就会删除该todo项目
### 5.删除项目
1. 1. 点击删除按钮,可以删除该todo项目
2. 点击删除按钮后会弹出确认框,询问用户是否确认删除
1. 1. 1. 点击确认则删除该项目
2. 点击取消则删除无效
### 6.全选/全不选
1. 1. 单击全选按钮,可以实现项目全选与全不选的切换
2. 单击按钮时,如果项目已全部被选中,则实现全不选效果
3. 单击按钮时,如果还有项目未被选中,则实现全选效果
4. 当项目全部被选中时,全选按钮高亮
### 5.info功能栏
1. 1. 有一个项目数量栏,显示处于未完成状态的项目总数
2. 有一个tab功能栏,可以切换项目列表的显示状态
1. 1. 1. 状态1,显示全部项目
2. 状态2,显示全部未完成项目
3. 状态3,显示全部完成项目
1. 1. 有一个批量删除按钮,可以一键删除所有完成项目
## (三)原型图

# 二、技术方案
## (一)技术选型
平台
- 操作系统: windows 10
- 开发平台: VSCode V16.2
- 测试平台: Chrome/FireFox -- 是否兼容IE6/7/8
- 文档平台: 语雀/ApiFox/Swagger
- 代码平台: github/gitee9
框架
- 前端Vue.js
- 后端Express
技术栈
- 前端: H5+CSS+Vue.js
- node+Express+MySQL
## (二)数据库设计
待办项目表(gotolist)
| 字段名 | 类型 | 属性 | 备注 |
| -------- | ------------- | ---------- | ---------------------------------- |
| id | int | 主键,自增 | id |
| content | varchar(255) | 非空 | todo项目内容 |
| comleted | tinyint | 非空 | todo项目状态(1表示完成,0表示待办) |
## (三)接口设计
### 1.baseURL
```
http://localhost:3000
```
### 2.查询所有项目
允许用户通过此接口查询已添加的所有todo项目。
请求
**资源路径:**`/toods`
**HTTP动词:**`GET`
**过滤信息:**无
响应
**返回结果**
```json
{
"code": 0,
"msg": "获取所有项目成功",
"result": [
{"id":1, "content": "todo1", "completed": "0"},
{"id":2, "content": "todo2", "completed": "0"},
{"id":3, "content": "todo3", "completed": "0"},
]
}
```
### 3.获取单个项目
允许用户通过此接口查询已添加的单个项目。
请求
**资源路径:**`/todos/:id`
**HTTP动词:**`GET`
**过滤信息:**无
响应
**返回结果**
```json
{
"code": 0,
"msg": "获取项目成功",
"result": {"id":1, "content": "待办1", "completed": "0"}
}
```
### 4.添加待办项目
允许用户通过此接口添加新的待办项目。
请求
**资源路径:**`/todos`
**HTTP动词:**`POST`
**过滤信息:**
```json
{
"content": "待办2"
}
```
响应
**返回结果**
成功的响应
```json
{
"code": 0,
"msg": "添加成功",
"result": {"id":1, "content": "待办1", "completed": "0"}
}
```
### 5.修改一个项目
允许用户通过此接口修改某个待办项目。
请求
**资源路径:**`/todos/:id`
**HTTP动词:**`PUT`
**过滤信息:**
```json
{
"content": "待办2",
"completed":1,
}
```
响应
**返回结果**
```json
{
"code": 0,
"msg": "更新成功",
"result": {"id":1, "content": "待办1", "completed": "2021-11-26 11:15:23"}
}
```
### 6.删除一个项目
允许用户通过此接口删除某个项目。
请求
**资源路径:**`/todos/:id`
**HTTP动词:**`DELETE`
**过滤信息:**无
响应
返回结果
```json
{
"code": 0,
"msg": "删除成功",
"result": ''
}
```
### 7.批量删除项目
允许用户通过此接口删除某个项目。
请求
**资源路径:**`/todos`
**HTTP动词:**`DELETE`
**过滤信息:**
```json
{
"completed":1,
}
```
响应
响应
返回结果
```json
{
"code": 0,
"msg": "删除成功",
"result": ''
}
```
# 三、搭建项目
## (一)搭建环境
### 1.创建项目
#### 1)使用`vue-cli`创建项目
在存放项目文件的文件夹中打开命令行窗口,在其中输入以下代码创建项目:
```javascript
vue create vue-todo
```
#### 2)在安装过程中手动进行一些设置





### 2.启动项目
#### 1)在vscode中启动项目
使用vscode打开`vue-todo`文件夹,在vscode的终端中输入如下命令:
```javascript
npm run serve
```
演示
.jpg)
#### 2)在浏览器中访问测试

### 3.清理项目
#### 1)清理App.vue

清理完成后的`App.vue`
```vue
```
#### 2)删除HellowWorld组件
## (二)项目分析
### 1.应用页面拆分

### 2.组件树

### 3.各部分大概功能
| 组件 | MainHeader | MainTodo | TodoItem | TodoInfo | MainTodo |
| ---- | ------------ | ---------------------- | ------------ | ---------------- | ---------- |
| 功能 | 实现项目标题 | 主体部分,实现所有功能 | 实现项目列表 | 实现各种功能按钮 | 写一些信息 |
## (三)实现整体结构
### 1.创建组件
1.在`components`目录下, 创建App.vue的三个子组件`MainHeader、MainTodo、MainFooter`
2.给组件命名
3.在模板中写字,方便进行测试
📚组件命名
1. 组件文件的命名使用驼峰法, eg: `MainHeader.vue`
2. 组件对象的名称跟文件名保持一致, eg: `{name: 'MainHeader'}`
3. 组件标签使用中横线的命名规范,eg:``
创建完成后的代码:
MainHeater.vue
```javascript
头部
```
MainTodo.vue
```javascript
主体
```
MainFooter.vue
```javascript
底部
```
### 2.导入组件
在`App.vue`中导入组件
```vue
```
### 3.挂载组件
在`App.vue`中挂载组件
```vue
```
### 4.引用组件
在`App.vue`的模板中引用三个子组件
```html
```
### 5.将MainTodo文件改造成目录
1)将MainTodo文件放到一个文件夹下,文件名改为`index.vue`
2)创建MainTodo组件的子组件
TodoItem.vue
```javascript
待办项
```
TodoInfo.vue
```javascript
功能面板
```
### 6.加载背景图片
1)编写reset样式
1. 在`assets`目录创建`styles\reset.styl`文件
2. 在`background-image`设置自己喜欢的背景图片
```css
*
margin: 0
padding: 0
body
font: 14px 'Microsoft Yahei'
// 背景图片
// style文件在的相对路径, 相对于当前的reset.styl这个文件
background-image: url('../images/nightfall.jpg')
// 背景图片位置固定
background-attachment: fixed
// 背景不要重复
background-repeat: no-repeat
// 背景位置居中
background-position: center center
// 背景覆盖整个viewport
background-size: cover
// 当背景没有加载时的颜色
background-color: #fff
```
\2) 加载到项目中
在`App.vue`中, 使用`@import`加载reset.styl文件
```vue
```
# 四、编写MainHeater组件
## (一)分析
### 1.描述
`MainHeater`组件的内容就是编写整个vue-tod案例的一个**标题**
示意图:

### 2.思路
#### (1)在`MainHeater`组件的模板中添加标题标签
1)添加唯一的根节点,这里选择使用语义化标签` `
📚语义化标签的好处:
1. 代码结构清晰,方便阅读,有利于团队合作开发。
2. 方便其他设备解析(如屏幕阅读器、盲人阅读器、移动设备)以语义的方式来渲染网页。
3. 有利于搜索引擎优化(SEO)。
2)在根节点中添加标题标签
#### (2)在`MainHeater`组件的`
```
### 3.创建单独的.styl文件存储一些公共样式
#### (1)在`styles`目录创建`theme.styl`, 定义颜色变量
```css
/* 主题颜色(变量) */
$red = rgb(254, 67, 101)
$lightred = rgb(252, 157, 154)
$yellow = rgb(249, 205, 173)
$ligthgreen = rgb(200, 200, 169)
$green = rgb(131, 175, 155)
```
#### (2)在`styles`目录创建`typeface.styl`, 定义字体样式
```css
/* 字体 */
$hp = STHupo // 华文琥珀:STHupo
$xw = STXinwei//华文新魏
$xh = STXihei//华文细黑
```
#### (3)在要使用的地方直接引入`.styl`文件
```css
```
## (三)图件
### 1.标题效果图

# 五、编写MainTodo组件的基本框架
## (一)分析
这一部分的内容主要是在`MainTodo/index.vue`中进行。由于`MainTodo`组件十分的复杂,在这一步我们要搭建它的基本框架,方便后续子组件的编写。
`MainTodo`组件大致可以拆分成如下部分:
①input输入框
内容为一个用来添加项目的输入框,在本部分编写。
②多个TodoItem组件
用于展示项目的列表部分,是后续将要编写的子组件。
③一个TodoInfo组件
用于控制项目列表的功能区,由多个按钮组成,是后续将要编写的子组件。
示意图:

## (二)思路
### 1.编写`Maintodo`组件的HTML框架
1)根据上一节分析的内容在`Maintodo`组件的模板中编写基本的html框架
- 输入框使用`input`标签
- TodoItem组件和TodoInfo组件等子组件,写组件标签
2)输入框自动聚焦的效果
期望效果:当页面加载完成后,鼠标会自动聚焦到输入框中。
实现方法:给``标签,添加一个`autofocus`属性。
📚autofocus属性的特点
autofocus 属性是一个布尔属性。
autofocus 属性规定当页面加载时,元素会自动获得焦点。
### 2.编写`Maintodo`组件整体的样式
这一部分样式的重点,我认为就是盒子的背景,盒子的背景需要与整个项目的背景图相协调。
这里我选择的是不设置背景色。由于我的背景图是一个风景(我的故乡),所以我希望能够完整的展现它,因此一开始,我给盒子设置了一个半透明的背景。但后来发现它与输入框的背景相重叠之后并不美观,而输入框是有默认的纯白背景色,必须手动添加半透明背景,因此我只能将盒子的背景去掉。
### 3.编写输入框的样式
输入框的样式是这一章当中最重要的部分,其中的重点有三个。
①输入框的背景
上面其实已经提到了这一点,输入框是有默认的纯白背景色,必须手动添加半透明背景。
②输入框中字体粗细的问题
浏览器有自带的字体,所有如果你没有手动设置字体的话,输入框中字体的粗细就调整不了。
③占位符的样式调整
由于我设置的背景比较亮,所以输入框占位符的默认颜色的对比度就不够强,所以这里我需要修改占位符的样式,这里需要使用一个伪类选择器`::placeholder`
## (三)效果实现
### 1.HTML部分
```html
```
### 2.`Maintodo`组件样式部分
```css
```
### 3.输入框样式部分
```css
```
# 六、编写TodoItem组件
## (一)分析
TodoItem组件其实就是视图中展示项目的列表,可以说是整个项目的核心部分,而在这一章中主要也就是完成一个框架的搭建,实现项目列表的静态页面,为后续编写增删改查功能做铺垫。
一个TodoItem组件可以分成三个部分:
- 勾选按钮
- 项目内容栏
- 删除按钮
示意图:

## (二)思路
### 1.编写`TodoItem`组件的HTML框架
根据上一节的分析TodoItem组件可以分成三个部分
①勾选按钮
按钮有许多实现方式,这里我选择使用复选框。
②项目内容栏
这里选择使用`label`标签,值得注意的是这里的`label`标签可以与复选框绑定,实现点击项目内容栏也能勾选的效果。但是这种写法也有弊端,那就是太容易被选中了,这样体验其实并不好,还可能会与后面的一些功能有冲突,所以这里就没有使用这种方法。
③删除按钮
删除按钮就直接使用一个按钮标签了
### 2.编写`TodoItem`组件各部分的CSS样式
#### 1)根节点`div`盒子的样式
这一部分样式的重点在于布局方式,由于根节点中有三个元素,因此它们的布局十分重要。我计划采用比较方便的`display:flex`弹性布局方式,并且在水平方向上采用`justify-content: space-between`的布局方式,使得勾选按钮位于盒子的最左边,项目内容栏位于盒子的中间,删除按钮位于盒子的最右边。
📚justify-content属性的常用属性值
- center 居中排列
- space-between 均匀排列每个元素,首个元素放置于起点,末尾元素放置于终点。
- space-around 均匀排列每个元素,每个元素周围分配相同的空间。
- sapce-evenly 均匀排列每个元素,每个元素之间的间隔相等。
#### 2)勾选按钮的样式
这一部分的重点就是一件事——李代桃僵。由于复选框的默认样式太丑了,因此我要将复选框隐藏,然后用一个
'✔'去代替它。'✔'灰暗时表示未选中,'✔'高亮使表示已选中。
这里我相到有两种写法:
方法一:使用`display:none`将复选框隐藏,此时复选框会脱标,在页面中不会占据位置,那么它的位置就使用一个与它绑定的`label`标签代替,'✔'就放到`label`标签中,高亮效果可以通过样式绑定来实现。
方法二:使用`appearance: none`将复选框隐藏,此时复选框不会脱标,在页面中依旧占据位置。接着在复选框中添加一个伪元素,'✔'就放到伪元素中,然后在创建一个选中状态下的伪元素,给它添加高亮的样式。
我选择使用了方法二,因为方法一相对比较繁琐。
#### 3)项目内容栏的样式
这一部分倒是没有什么值得说的,只有一点,给它添加`flex:1`,让`label`标签占据盒子中剩余的所有空间。
📚flex属性的用法
flex 属性用于设置或检索弹性盒模型对象的子元素如何分配空间。
#### 4)删除按钮的样式
这里依旧是有三个重点
①和勾选按钮一样这里也要进行李代桃僵,用一个'×'去替代`button`标签,不同的是这里要实现只有鼠标悬停在`TodoItem`组件上时才显示删除按钮的效果,因此伪元素要创建在,伪类选择器`:hover`下的`button`标签中
②`clearDefault()`,这是将css样式封装到一个函数中,可以实现样式的复用
③`background-color: transparent`,这个样式的意思是,使背景色透明
### 3.编写JS逻辑
`TodoItem`组件,只表示一个项目,而只有在它的父组件`MainTodo`组件中才知道一共要渲染多少个子组件,以及每个子组件中的内容。因此这里的js逻辑的编写核心主要有两大部分:
第一部分:列表渲染,在父组件中使用列表渲染,渲染子组件
第二部分:父传子,父组件将项目内容传给子组件,放到子组件的`label`标签中
## (三)效果实现
### 1.HTML部分
```html
```
### 2.CSS样式部分
#### 1)根节点`div`盒子的样式
```css
```
#### 2)勾选按钮(复选框)的样式
```css
```
#### 3)项目内容栏(label)的样式
```css
```
#### 4)删除按钮的样式
```css
```
### 3.JS逻辑部分
#### 1)在父组件中创建一个数组,用来储存项目信息
```javascript
data() {
return {
todos: [
{ id: 1, content: '待办1', completed: 0 },
{ id: 2, content: '待办2', completed: 0 },
{ id: 3, content: '待办3', completed: 0 },
],
}
},
```
#### 2)利用`todos`数组,对`TodoItem`组件标签进行列表渲染
```javascript
```
#### 3)在父组件中向子组件传值
在父组件从给子组件的标签绑定一个自定义属性
```javascript
```
在子组件中通过`props`配置项接收数据
```javascript
props: {
todoItem: {
type: Object,
require: true,
},
},
```
#### 4)将项目内容使用插值语法放到子组件的`label`标签中
```html
```
## (四)图件
### 1.渲染项目列表的基本逻辑

# 七、实现后端接口
## (一)项目搭建
### 1.安装express脚手架
```shell
npm i express-generator -g
```
### 2.通过脚手架搭建项目
1)将存放后台项目的文件文件夹用VScode打开
2)在终端中输入命令创建后台目录todo-api
```shell
express --no-view todo-api
```
### 3.安装相关依赖
在后端项目的根目录`todo-api`下,执行如下命令
1)安装开发环境依赖
```shell
npm i nodemon -D
```
2)安装mysql依赖
```shell
npm i mysql
```
1)安装cors中间件
```shell
npm i cors
```
1)安装所有依赖
```shell
npm i
```
### 4.启动项目
1)修改`package.json`, 使用`nodemon`启动项目
```json
"scripts": {
"start": "node ./bin/www"
},
"scripts": {
"start": "nodemon ./bin/www"
},
```
2)运行后台
```json
npm run start
```
3)测试

### 5.注册cors中间件
1)修改`app.js`
```javascript
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
// 1.导入cors中间件的包
const cors = require("cors");
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var app = express();
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
// 2.要在注册路由之前,注册全局中间件
app.use(cors());
app.use("/", indexRouter);
app.use("/users", usersRouter);
module.exports = app;
```
### 6.规划路由
1)创建路由模块
在`routes`目录下, 创建`todos.js`文件
2)编写路由文件(测试)
```javascript
// 1.导入express
const express = require("express");
// 2.实例化router对象
const router = express.Router();
// 3.编写路由规则
// 用于测试
router.get("/", (req, res) => {
res.send("todos");
});
// 4.导出router对象
module.exports = router
```
3)导入路由对象
在`app.js`中导入路由对象
```javascript
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
// 1.1 导入cors中间件的包
const cors = require("cors");
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
// 2.1 导入todosRouter对象
const todosRouter = require("./routes/todos");
var app = express();
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
// 1.2 要在注册路由之前,注册全局中间件
app.use(cors());
app.use("/", indexRouter);
app.use("/users", usersRouter);
// 2.2 注册路由
app.use("/todos", todosRouter);
module.exports = app;
```
4)测试

### 7.编写数据库模块
1)编写mysql默认配置文件
创建目录`todo-api/config/config.default.js`
编写`config.default.js`文件
```javascript
module.exports = {
// 标记是否处于测试环境
debug: true,
// 根据mysql数据库相关的配置
mysql: {
// 域名
host: "127.0.0.1",
// 端口
port: 3306,
// 用户名
user: "root",
// 密码
password: "1234",
// 数据库名
database: "db202203",
},
};
```
2)创建数据库模块目录
创建目录`todo-api/db/index.js`
3)编写`index.js`文件
```javascript
// 1.导入mysql包
const mysql = require("mysql");
// 2.导入数据库配置文件
const { mysql: MySQLCon } = require("../config/config.default");
// 3.创建数据库连接
const con = mysql.createConnection( MySQLCon);
// 4.连接数据库
con.connect();
// 5.编写操作
// 5.1 获取所有数据
function getAll(sql) {
return new Promise((resolve, reject) => {
con.query(sql, (err, data) => {
if (err) reject(err);
resolve(data);
});
});
}
// 5.2 获取单个数据
function getOne(sql) {
return new Promise((resolve, reject) => {
con.query(sql, (err, data) => {
if (err) reject(err);
// 是否查询到项目的校验,如果查询到了项目就返回查询到的内容,否则返回null
data.length != 0 ? resolve(data[0]) : resolve(null);
});
});
}
// 5.3 增删改
function exec(sql) {
return new Promise((resolve, reject) => {
con.query(sql, (err, data) => {
if (err) reject(err);
resolve(data);
});
});
}
// 6.导出三个方法
module.exports = {
getAll,
getOne,
exec,
};
```
4)在`todos.js`文件中, 加载数据库操作的方法
```javascript
// 1.导入express
const express = require("express");
// 导入数据库操作的方法
const { getAll, getById, exec } = require("../db");
// 2.实例化router对象
const router = express.Router();
// 3.编写路由规则
// 用于测试
router.get("/", (req, res) => {
res.send("todos");
});
// 4.导出router对象
module.exports = router;
```
### 8.实现获取所有待办事项
#### (1)编写router/todos.js文件
1)路由模块中查询全部的完整代码
```javascript
// 查询所有
router.get('/', async function(req, res) {
// 获取请求的参数
// 编写sql语句
let sql = `select * from vue_todos`
// 执行sql语句
// 处理异常
try {
const data = await getAll(sql)
res.send({
code:0,
msg:'获取所有代码成功',
result:data
})
} catch (error) {
res.send({
code:100101,
msg:'获取所有代码失败',
result:debug ? error : ''
})
}
});
```
2)代码流程示意图

📌相关知识点
1.`debug ? error : ''`
这是之前讲过的三元运算符,相当于一个选择语句。
表达的含义是:如果`debug == ture`则`res.send({result:error})`
如果`debug == false`则`res.send({result:''})`
2.异常的两种处理方式
(1)`throw`
抛出异常,一般是抛出一个异常对象(error)
如果不进行处理的话,程序就会停止运行,后面的代码就无法执行
(2)`try-catch`
可以让程序在出了异常之后,后续代码能够正常执行,增强程序的健壮性
语法:
try{
可能会出现异常的代码
}catch(error){
处理异常对象的代码
}
3.错误代码的含义
例:100101
10代表第一个版本,01代表todos项目,01代表第一个错误
### 9.实现根据id获取单个待办事项
#### (1)编写router/todos.js文件
1)查询一个的完整代码
```javascript
// 根据id查询一个
router.get('/:id', async function(req, res) {
// 获取请求的参数
const id = req.params.id
// 对参数的格式进行校验
const reg = /^\d+$/ //正则,表示内容为数字
if (reg.test(id) == false) {
res.send({
code:100104,
msg:'id必须为数字',
result:''
})
return
}
try {
// 编写sql语句
let sql = `select * from vue_todos where id=${id}`
// 执行sql语句
const data = await getOne(sql)
if(data) {
res.send({
code:0,
msg:'获取单个项目成功',
result:data
})
}else{
res.send({
code:100102,
msg:'id对应的数据不存在',
result:''
})
}
} catch (error) {
res.send({
code:100103,
msg:'获取单个项目失败',
result:debug ? error : ''
})
}
});
```
2)代码流程示意图

📌相关知识点
正则
概念:使用单个字符串来描述、匹配一系列符合某个句法规则的字 符串搜索模式。
语法:`/正则表达式主体/修饰符(可选)`
解析之前使用的正则:`/^\d+$/`
/ /代表字符串
^代表以什么开头
$代表什么结尾
\d代表数字
+代表出现一次或多次
test()
test() 方法用于检测一个字符串是否匹配某个模式,如果字符串中有匹配的文本,则返回 true,否则返回 false。
### 10.实现添加待办事项
#### (1)编写router/todos.js文件
1)添加的完整代码
```javascript
/ 3.3 添加
router.post("/", async function (req, res) {
// 获取请求的参数
const { content } = req.body;
// 对请求参数进行校验(不为空或者不为null)
if (!content) {
res.send({
code: 100106,
msg: "content为空",
result: "",
});
return;
}
// 编写sql语句
let sql = `insert into vue_todos (content) values ('${content}')`;
// 执行sql语句
try {
const data = await exec(sql);
res.send({
code: 0,
message: "新增项目成功",
result: {
id: data.insertId,
content,
completed: 0,
},
});
} catch (error) {
res.send({
code: 100105,
msg: "添加项目失败",
result: debug ? error : "",
});
}
});
```
2)代码流程示意图

### 11.实现修改待办事项
#### (1)编写router/todos.js文件
1)修改的完整代码
```javascript
//修改
router.put('/:id', async function(req, res) {
// 获取请求的参数
const id = req.params.id
const {content,completed} = req.body
// 校验请求参数
// 校验id
const reg = /^\d+$/ //正则,表示内容为数字
if (reg.test(id) == false) {
res.send({
code:100108,
msg:'id必须为数字',
result:''
})
return
}
// 编写sql语句
let sql = `update vue_todos set content='${content}', completed=${completed} where id=${id}`
// 执行sql语句
try {
await exec(sql)
res.send({
code: 0,
message: "修改待办成功",
result: {
id,
content,
completed,
},
});
} catch (error) {
res.send({
code:100110,
msg:'更新待办失败',
result:debug ? error : ''
})
}
});
```
2)代码流程示意图

### 12.实现全选/全不选所有项目
#### (1)编写router/todos.js文件
1)完整代码
```javascript
//实现全选、全不选
router.put('/', async function(req, res) {
// 获取请求的参数
const { completed } = req.query;
// 获得过滤条件
let condition = 0;
if (completed == 0) {
condition = 1;
}
// 编写sql语句
let sql = `update vue_todos set completed=${completed} where completed=${condition}`
// 执行sql语句
try {
const data = await exec(sql)
res.send({
code: 0,
message: "修项目成功",
result: {
completed,
},
});
} catch (error) {
res.send({
code:100113,
msg:'修改项目失败',
result:debug ? error : ''
})
}
})
```
### 13.实现删除一个
#### (1)编写router/todos.js文件
```javascript
router.delete("/:id", async (req, res) => {
// 一. 解析请求参数
const { id } = req.params;
// 二. 操作数据库
let sql = `delete from vue_todos where id=${id}`;
await exec(sql);
// 三. 返回结果
res.send({
code: 0,
message: "删除待办成功",
result: "",
});
});
```
### 14.实现批量删除
#### (1)编写router/todos.js文件
```javascript
router.delete("/", async (req, res) => {
// 一. 解析请求参数
const { completed } = req.query;
// 二. 操作数据库
let sql = `delete from vue_todos where completed=${completed}`;
await exec(sql);
res.send({
code: 0,
message: "清除已完成的待办",
result: "",
});
});
```
# 八、实现项目列表动态展示的功能
## (一)分析
在第三章中我们通过列表渲染和父传子初步完成了项目列表的功能,但是它只是静态的,数据存储在
`todos`数组中。在这一章中我们要将`todos`数组与数据库相连接实现列表的动态展示。
## (二)思路
### 1.核心问题
①如何建立与数据库的连接?
按照之前的经验,在js中是通过`ajax`向数据库发送请求,从而连接数据库的。
📚在Vue项目中, 一般使用`axios`库, 发送`ajax`请求
②应该在何时发送`ajax`请求?
我们要实现在页面加载时就能够从数据库中获取项目信息,然后写入`todos`数组中,从而完成页面的初始渲染。因此,就要用到生命周期函数`created()`,在组件的创建阶段结束后,发送`ajax`请求,实现数据的初始化。
### 2.大概步骤
1)安装`axios`插件
2)创建一个函数,在函数中发送`get`请求获取全部的项目数据,并将数据写入`todos`数组中
3)在生命周期函数`created()`中调用上一步创建的函数
## (三)效果实现
### 1.安装`axios`插件
#### 1)在终端中输入以下代码安装插件`axios`和`vue-axios`
- `axios`是用于在`vue`中发送`ajax`请求
- `vue-axios`是用于辅助`axios`发送请求
💡`vue-axios`的具体作用:
`vue-axios`的作用是将`axios`对象挂载在原形对象`Vue`,这样我只需导入一次就能够在所有的组件中使用`http`方法发送`ajax`请求。如果不安装该插件的话,那么在每一需要发送`ajax`请求的组件中都需要导入一次`axios`。
```javascript
npm i axios vue-axios
```
#### 2)在`main.js`文件中导入
```javascript
// 导包
import axios, { Axios } from "axios";
import VueAxios from "vue-axios";
```
#### 3)在`main.js`文件中使用插件
```javascript
// 使用插件(调用插件对象的install方法,在Vue的原形对象上,挂载了$http)
Vue.use(VueAxios, axios);
```
### 2.创建一个函数`getAll`用来获取所有项目数据,并写入todos数组
```javascript
getAll() {
this.$http.get("http://localhost:3000/todos").then((res) => {
// 结构res.data
const { code, message, result } = res.data;
if (code == 0) {
this.todos = result;
}
});
},
```
### 3.在生命周期函数`created()`中调用`getAll`函数
```javascript
created() {
// 发送ajax请求
this.getAll();
},
```
## (四)图件
### 1.项目列表动态展示功能的逻辑示意图

# 九、实现添加项目功能
## (一)分析
在这一章节中我们要完成增删改查中的增加功能。大概要达到以下的效果
1. 在页面的输入框中输入文本,按回车后可以添加到下面的项目列表中
2. 添加项目时数据库和`todos`数组中的数据,也相应的要发生改变
3. 当输入的内容为空时,弹出一个弹窗提醒
4. 添加成功后要清空输入框中的内容
示意图:

## (二)思路
### 1.按回车添加项目
在上一节的分析中的1、2两条其实是互为表里的,列表中添加项目时表,操纵数据库是里。
大概的步骤是:
1)监听按下回车的键盘事件
达成这个效果,要使用按键修饰符`.enter`
2)在事件处理函数中发送`post`请求
在这一步中有一个问题,我们需要将输入框中的内容作为请求参数发送出去,那么如何获取输入框中的值呢?
这里有两种方法:
方法一,通过事件对象获得,`e.target.value`
方法二,通过双向绑定获得,创建一个变量,将其与输入框进行双向绑定,输入框中的内容就储存在变量中
但是第一种方法是直接操纵DOM的,在vue中不建议使用,所以我在这里使用的是方法二。
3)在事件处理函数中调用`getAll`函数重新渲染页面
### 2.弹窗提示
在发送`post`请求之前进行一个判断,如果输入框的内容为空,就发出弹窗提示
### 3.清空输入框
让绑定的变变量等于一个空字符串`双向绑定的变量 = ''`
## (三)效果实现
### 1.给输入框绑定键盘事件
```html
```
### 2.对输入框进行双向绑定
1)双向绑定
```html
```
2)创建`content`变量
```javascript
data() {
return {
todos: [],
// 存储输入框中的内容
content: "",
};
},
```
### 3.编辑事件处理函数`handleAdd`
```javascript
// 添加项目
handleAdd() {
// 弹窗提示
if (this.content == "") {
alert("您还没有输入内容呢!");
return;
}
// 发送post请求
this.$http
.post("http://localhost:3000/todos", { content: this.content })
.then((res) => {
// 重新渲染页面
this.getAll();
// 清空输入框
this.content = "";
});
},
```
## (四)图件
### 1.添加项目功能的逻辑示意图

# 十、实现修改功能——勾选框逻辑
## (一)分析
勾选框的逻辑,主要是要达成一表一里两方面的效果
1)里就是实现勾选框与项目`completed`属性的双向绑定,
- 勾选框是否选中,取决于项目的`completed`属性值
- 当勾选框的选择状态发生变化时,项目的`completed`属性值也随之发生变化
2)表就是勾选框的选中状态与项目内容栏中文本的样式相绑定
- 当勾选框选中时,在项目内容栏中的文本上添加删除线
- 当勾选框选未中时,在项目内容栏中的文本无删除线
示意图:

逻辑图:

## (二)思路
### 1.实现勾选框与项目`completed`属性的双向绑定
1)在`todoItem`组件中对勾选框的`checked`属性进行属性绑定`:checked`
这个地方就有一个问题,属性绑定的指令表达式应该是该项目的`completed`属性值,我们如何拿到他呢?
这里有三种方法:
方法一:直接绑定`todoItem.completed`,由于这里只是进行读操作,因此直接将`props`中的值进行绑定也无妨
方法二:对`props`中的值进行一下处理,创建一个data变量,将props中的值作为它的初始值
```javascript
data(){
return {
check:this.todoItem.completed,
}
}
```
方法三:使用 `prop` 的值来定义一个计算属性
```javascript
computed: {
check() {
return this.todoItem.completed;
},
},
```
可以参考官方文档:[Vue.js单向数据流](https://v2.cn.vuejs.org/v2/guide/components-props.html#单向数据流)
2)在`todoItem`组件中监听勾选框的`change`事件`@change`
### 2.在子组件中将项目`id`传给父组件
在上一步所绑定的`change`事件的指令表达式中,使用`$emit`给父组件发送一个自定义事件,以及作为参数的项目`id`,这里的项目`id`可以从第三章当中父组件传给子组件的`todoItem`对象中获得。
### 3.在父组件中监听自定义事件
### 4.编辑自定义事件的事件处理函数
在事件处理函数中要完成几件事情:
1)接收数据
给事件处理函数,定义一个参数,通过这个参数就可以接收上一步中发送的项目`id`数据。
2)根据`id`修改对应项目的`completed`属性
方法是对存储数据的`todos`数组进行过滤,拿到`id`对应的项目对象,修改该对象的`completed`属性。
在这一步中的过滤数组有两种方法,使用`filter()`方法和使用`find()`方法
📚`filter()`方法和`find()`方法
`filter()`方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
参数:必须。函数,数组中的每个元素都会执行这个函数
返回值:一个新的数组
`find()`方法返回通过测试(函数内判断)的数组的第一个元素的值。
参数:必须。函数,数组中的每个元素都会执行这个函数
返回值:返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 `undefined`。
3)发送`put`请求
注意要将`id`和对应的项目对象作为请求参数。
4)在回调函数中再次调用`getAll`函数重新渲染页面
### 5.实现项目内容文本样式的动态绑定
1)创建一个类,里面包含给文本添加删除线的样式
2)在`todoItem`组件的根节点上,进行样式的动态绑定
利用项目的`completed`属性写一个三元表达式`todoItem.completed ? '删除线样式类' : ''`
## (三)效果实现
### 1.对子组件中的复选框进行双向绑定
```html
```
### 2.在父组件中监听自定义事件`fulfill`
```html
```
### 3.编辑事件处理函数`checkChange`
```javascript
/ 实现复选框逻辑,更新项目的completed属性
checkChange(id) {
// 拿到被点击的那个todoItem
const data = this.todos.filter((item) => item.id == id);
data[0].completed == 1
? (data[0].completed = 0)
: (data[0].completed = 1);
this.$http
.put("http://localhost:3000/todos/" + id, data[0])
.then((res) => {
const { code, message, result } = res.data;
if (code == 0) {
this.getAll();
}
});
},
```
### 4.实现项目内容文本样式的动态绑定
1)创建一个类,里面包含给文本添加删除线的样式
```css
```
#### 3)优化1-列表渲染tab按钮
创建数组,保存按钮名称
```javascript
data() {
return {
btns: ["all", "active", "completed"],
};
},
```
优化模板
```javascript
```
#### 4)优化2-样式封装复用
创建一个文件`assets\styles\mixins.styl`,在其中封装样式
```javascript
// mixin:混入(函数)
clearDefault()
appearance: none
border: none
outline: none
btn(c,border = false)
padding 5px 10px
border: 1px solid c
color: #fff
border-radius: 5px
cursor pointer
if (border == false)
background-color c
color #fff
else
color: #000
primaryBtn()
btn(rgb(249, 205, 173))
primaryBorderBtn()
btn(rgb(249, 205, 173),true)
infoBtn()
btn(rgb(131, 175, 155))
```
使用封装的函数
原来的样式代码
```css
.clear
padding 5px 10px
border: 1px solid $green
background-color $green
color: #fff
border-radius: 5px
cursor pointer
```
使用样式函数后的代码
```css
.clear
infoBtn()
```
### 2.实现项目数量栏功能
**父组件**
#### 1)在计算属性中获得待办项目的数量
```javascript
computed: {
// 表示处于待办状态(即completed=0)的项目的数量
activeNumber() {
return this.todos.filter((item) => item.completed == 0).length;
},
},
```
#### 2)模板中给子组件标签绑定自定义属性,实现父传子
```html
```
**子组件**
#### 3)在props选项中接收数据
```javascript
props: {
total: {
type: Number,
require: true,
},
},
```
#### 4)将接收的数据在模板中应用
```html
{{ total }} items left
```
### 3.实现tab选项卡的功能
**子组件**
#### 1)创建一个变量,用来保存按钮名称
```javascript
data() {
return {
btns: ["all", "active", "completed"],
clicked: "all",
};
},
```
#### 2)实现点击高亮的效果
编写高亮样式
```css
.current
background-color rgba(249, 205, 173,.7)
```
绑定高亮样式
```html
```
#### 3)给a标签绑定点击事件
```html
```
#### 4)在事件处理函数中设置clicked的值,并进行子传父
```javascript
methods: {
handleClick(text) {
this.clicked = text;
this.$emit("alter", text);
},
},
```
**父组件**
#### 5)监听自定义事件,接收数据
```html
```
#### 6)在事件处理函数中对todos数组进行过滤
创建一个数组来储存过滤后的数据
```javascript
data() {
return {
todos: [],
// 存储输入框中的内容
content: "",
filterTodos: [],
};
},
```
编辑事件处理函数
```javascript
alterInfo(text) {
switch (text) {
case "all":
this.filterTodos = this.todos;
break;
case "active":
this.filterTodos = this.todos.filter((item) => item.completed == 0);
break;
case "completed":
this.filterTodos = this.todos.filter((item) => item.completed == 1);
break;
default:
this.filterTodos = this.todos;
}
},
```
7)使用过滤后的数组替代todos进行页面渲染
使用`filterTodos`进行`todo-item`的列表渲染
```javascript
```
在`getAll`函数中更新`filterTodos`实现初始渲染
```javascript
getAll() {
this.$http.get("http://localhost:3000/todos").then((res) => {
// 结构res.data
const { code, message, result } = res.data;
if (code == 0) {
this.todos = result;
this.filterTodos = result
}
});
},
```
### 4.实现批量删除按钮的内容
#### 1)监听按钮的点击事件,进行子传父
```html
```
#### 2)父组件中监听自定义事件clear
```html
```
#### 3)在数据处理函数clearCompleted中发送请求删除所有完成项目
```javascript
clearCompleted() {
this.$http
.delete("http://localhost:3000/todos?completed=1")
.then((res) => {
const { code, message, result } = res.data;
if (code == 0) {
this.getAll();
}
});
},
```
## (四)`active`和`completed`状态下的项目数量栏问题
### 1.描述
在实现上述效果后发现一个问题,在tab栏的三种不同的状态下,项目数量栏显示的数量都是一样。我认为理想中的效果应当是这样的:
- all状态 显示全部项目中待办项目的数量
- active状态 显示全部项目中待办项目的数量
- completed状态 显示数量为0
### 2.解决方法
这里的解决方法很简单,只要在计算属性的时候将参与计算的属性,由`todos`替换为`flitertodos`即可。由于`flitertodos`数组中的数据在三种状态下都各不相同,根据其计算出的新属性,在三种状态下也会有差异
### 3.效果展示

## (五)`active`和`completed`状态下增删改问题
### 1.描述
在完成‘点击tab切换显示’的效果后,发现一个bug:
在`active`和`completed`状态(显示的项目是经过过滤的,`active`状态下显示所有待办项目;`completed`状态下显示所有已经完成的项目)下进行增删改之后,会重新获取全部的项目,而不是之前`active`和`completed`状态下经过过滤的项目。
### 2.原因
造成这个bug的原因是因为,之前为了进行数据初始化,在`getAll`函数中对`filterTodos`进行了赋值操作(`this.filterTodos = this.todos`)。而在进行增删改时都会调用`getAll`函数对页面进行重新渲染,
此时就会改变`filterTodos`的值,使其包含全部的待办。而由于待办项目是依赖`filterTodos`渲染的,所以只要进行增删改页面就会显示所有待办,无法实现只显示处于待办或完成状态的项目。
bug示意图:

### 3.解决
**分析**
解决这个问题必须要同时满足以下条件
1. 为了实现数据初始化,必须在`getAll`函数中`AJAX`的回调里对`flitertodos`数组进行初始化赋值。
2. 在进行增删改操作后必须要重新渲染页面
3. 根据tab栏中所选择的选项卡,页面中要呈现相应的效果
4. 页面必须根据`fliterTodos`数组进行渲染
5. 页面在`active`和`completed`状态下,进行增删改的操作后,必须显示的还是待完成&已完成的项目
解决方案
1)创建一个变量`state`用来表示tab栏的状态,指示应该`fliterTodos`数组赋予什么样的数据
- `state="all"` --显示所有项目(待办+完成)
- `state="active"` --显示所有待办项目
- `state="completed"` --显示所有完成项目
```javascript
data() {
return {
todos: [],
content: "",
filterTodos: [],
state: "all",
};
},
```
2)创建一个函数`updateFilterTodos`,根据`state`的值,给`fliterTodos`数组赋值
```javascript
updateFilterTodos() {
// 更新filterTodos的值
switch (this.state) {
case "all":
this.filterTodos = this.todos;
break;
case "active":
this.filterTodos = this.todos.filter((item) => item.completed == 0);
break;
case "completed":
this.filterTodos = this.todos.filter((item) => item.completed == 1);
break;
default:
this.filterTodos = this.todos;
}
},
```
3)在`getAll`函数中,调用`updateFilterTodos`函数,给`fliterTodos`数组赋值
原来的代码:
```javascript
getAll() {
this.$http.get("http://localhost:3000/todos").then((res) => {
// 结构res.data
const { code, message, result } = res.data;
if (code == 0) {
this.todos = result;
this.fliterTodos = this.todos;
}
});
},
```
优化后的代码:
```javascript
getAll() {
this.$http.get("http://localhost:3000/todos").then((res) => {
// 结构res.data
const { code, message, result } = res.data;
if (code == 0) {
this.todos = result;
this.updateFilterTodos();
}
});
},
```
4)在其他的地方也可以复用这个函数
```javascript
// 实现tab栏切换界面效果
alterInfo(text) {
this.state = text;
this.updateFilterTodos();
},
```
## (六)图件
**active和completed状态下增删改问题相关图件:**
### 1.通过filterTodos渲染视图的流程图(总图)
.jpeg)
### 2.浏览器页面初次渲染流程图

### 3.进行增删改之后页面重新渲染的流程图

### 4.使用tab栏按钮实现界面切换时页面渲染的流程图

**组件基本功能相关图件:**
### 5.项目数量栏功能逻辑图

### 6.tab选项卡功能逻辑图

### 7.批量删除功能逻辑图

### 8.tab选项卡功能最终效果图

### 9.批量删除功能最终效果图

# 十四、实现全选功能
## (一)描述
设计一个全选/全不选按钮,它需要实现以下效果:
1. 点击按钮后所有项目都呈现被选中的效果,如果点击时所有项目已经被选中,则呈现全不选效果
2. 点击按钮实现全选效果时,更新数据库使,所有项目的`completed`属性为1,反之,所有项目的`completed`属性为0
3. 只要视图中的项目全部处于选中状态(无论是通过全选按钮还是勾选按钮又或是其他方式),全选按钮都会高亮显示,反之,全选按钮不会高亮
效果示意图:

## (二)思路
### 1.在`MainTodo`组件的模板中添加一个按钮
这里的按钮其实有多种实现方式,比如说使用`button`标签、`input`标签、`a`标签等。
我在这里借鉴了Evan You的写法,使用` +