# 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 # 一、需求分析 ## (一)系统框架 ![01-框架图](images/01-框架图.jpeg) ## (二)具体的业务需求 ### 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. 有一个批量删除按钮,可以一键删除所有完成项目 ## (三)原型图 ![02-vue-todo项目原型图](images/02-vue-todo项目原型图.jpg) # 二、技术方案 ## (一)技术选型 平台 - 操作系统: 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)在安装过程中手动进行一些设置 ![03-创建项目-选择手动设置](images/03-创建项目-选择手动设置.jpg) ![04-搭建项目-选择支持](images/04-搭建项目-选择支持.jpg) ![05-创建项目-设置2](images/05-创建项目-设置2.jpg) ![06-搭建项目-选择css预处理器](images/06-搭建项目-选择css预处理器.jpg) ![06-搭建项目-选择css预处理器](images/06-搭建项目-选择css预处理器.jpg) ### 2.启动项目 #### 1)在vscode中启动项目 使用vscode打开`vue-todo`文件夹,在vscode的终端中输入如下命令: ```javascript npm run serve ``` 演示 ![08-启动项目-启动演示(更新)](images/08-启动项目-启动演示(更新).jpg) #### 2)在浏览器中访问测试 ![09-启动项目-浏览器测试](images/09-启动项目-浏览器测试.jpg) ### 3.清理项目 #### 1)清理App.vue ![10-清理项目-清理app.vue](images/10-清理项目-清理app.vue.jpg) 清理完成后的`App.vue` ```vue ``` #### 2)删除HellowWorld组件 ## (二)项目分析 ### 1.应用页面拆分 ![11-搭建项目-界面拆分](images/11-搭建项目-界面拆分.jpg) ### 2.组件树 ![12-项目树](images/12-项目树.jpeg) ### 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案例的一个**标题** 示意图: ![13-编写mainheater组件-示意图](images/13-编写mainheater组件-示意图.jpg) ### 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.标题效果图 ![14-编写mainheater组件-最终效果图](images/14-编写mainheater组件-最终效果图.jpg) # 五、编写MainTodo组件的基本框架 ## (一)分析 这一部分的内容主要是在`MainTodo/index.vue`中进行。由于`MainTodo`组件十分的复杂,在这一步我们要搭建它的基本框架,方便后续子组件的编写。 `MainTodo`组件大致可以拆分成如下部分: ①input输入框 内容为一个用来添加项目的输入框,在本部分编写。 ②多个TodoItem组件 用于展示项目的列表部分,是后续将要编写的子组件。 ③一个TodoInfo组件 用于控制项目列表的功能区,由多个按钮组成,是后续将要编写的子组件。 示意图: ![15-编写maintodo组件的基本框架-框架示意图](images/15-编写maintodo组件的基本框架-框架示意图.jpg) ## (二)思路 ### 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组件可以分成三个部分: - 勾选按钮 - 项目内容栏 - 删除按钮 示意图: ![16-编写todoitem-示意图](images/16-编写todoitem-示意图.jpg) ## (二)思路 ### 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.渲染项目列表的基本逻辑 ![17-渲染项目列表的基本逻辑](images/17-渲染项目列表的基本逻辑.jpeg) # 七、实现后端接口 ## (一)项目搭建 ### 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)测试 ![18-实现后台接口-测试](images/18-实现后台接口-测试.jpg) ### 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)测试 ![19-实现后台接口-路由测试](images/19-实现后台接口-路由测试.jpg) ### 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)代码流程示意图 ![20-获取全部接口代码流程图](images/20-获取全部接口代码流程图.jpeg) 📌相关知识点 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)代码流程示意图 ![21-获取一个接口代码流程图](images/21-获取一个接口代码流程图.jpeg) 📌相关知识点 正则 概念:使用单个字符串来描述、匹配一系列符合某个句法规则的字 符串搜索模式。 语法:`/正则表达式主体/修饰符(可选)` 解析之前使用的正则:`/^\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)代码流程示意图 ![22-新增接口代码流程图](images/22-新增接口代码流程图.jpeg) ### 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)代码流程示意图 ![23-修改接口代码流程图](images/23-修改接口代码流程图.jpeg) ### 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.项目列表动态展示功能的逻辑示意图 ![24-项目列表动态展示功能的逻辑示意图](images/24-项目列表动态展示功能的逻辑示意图.jpeg) # 九、实现添加项目功能 ## (一)分析 在这一章节中我们要完成增删改查中的增加功能。大概要达到以下的效果 1. 在页面的输入框中输入文本,按回车后可以添加到下面的项目列表中 2. 添加项目时数据库和`todos`数组中的数据,也相应的要发生改变 3. 当输入的内容为空时,弹出一个弹窗提醒 4. 添加成功后要清空输入框中的内容 示意图: ![25-实现添加效果-示意图](images/25-实现添加效果-示意图.gif) ## (二)思路 ### 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.添加项目功能的逻辑示意图 ![26-添加项目功能的逻辑示意图](images/26-添加项目功能的逻辑示意图.jpeg) # 十、实现修改功能——勾选框逻辑 ## (一)分析 勾选框的逻辑,主要是要达成一表一里两方面的效果 1)里就是实现勾选框与项目`completed`属性的双向绑定, - 勾选框是否选中,取决于项目的`completed`属性值 - 当勾选框的选择状态发生变化时,项目的`completed`属性值也随之发生变化 2)表就是勾选框的选中状态与项目内容栏中文本的样式相绑定 - 当勾选框选中时,在项目内容栏中的文本上添加删除线 - 当勾选框选未中时,在项目内容栏中的文本无删除线 示意图: ![27-实现勾选框逻辑-示意图](images/27-实现勾选框逻辑-示意图.gif) 逻辑图: ![28-勾选框逻辑图](images/28-勾选框逻辑图.jpeg) ## (二)思路 ### 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.效果展示 ![43-实现todoinfo组件-待办项目数量栏效果图](images/43-实现todoinfo组件-待办项目数量栏效果图.gif) ## (五)`active`和`completed`状态下增删改问题 ### 1.描述 在完成‘点击tab切换显示’的效果后,发现一个bug: 在`active`和`completed`状态(显示的项目是经过过滤的,`active`状态下显示所有待办项目;`completed`状态下显示所有已经完成的项目)下进行增删改之后,会重新获取全部的项目,而不是之前`active`和`completed`状态下经过过滤的项目。 ### 2.原因 造成这个bug的原因是因为,之前为了进行数据初始化,在`getAll`函数中对`filterTodos`进行了赋值操作(`this.filterTodos = this.todos`)。而在进行增删改时都会调用`getAll`函数对页面进行重新渲染, 此时就会改变`filterTodos`的值,使其包含全部的待办。而由于待办项目是依赖`filterTodos`渲染的,所以只要进行增删改页面就会显示所有待办,无法实现只显示处于待办或完成状态的项目。 bug示意图: ![44-bug示意图](images/44-bug示意图.jpeg) ### 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渲染视图的流程图(总图) ![45-通过filterTodos渲染视图的流程图(总图)](images/45-通过filterTodos渲染视图的流程图(总图).jpeg) ### 2.浏览器页面初次渲染流程图 ![46-浏览器页面初次渲染流程图](images/46-浏览器页面初次渲染流程图.jpeg) ### 3.进行增删改之后页面重新渲染的流程图 ![47-进行增删改之后页面重新渲染的流程图](images/47-进行增删改之后页面重新渲染的流程图.jpeg) ### 4.使用tab栏按钮实现界面切换时页面渲染的流程图 ![48-使用tab栏按钮实现界面切换时页面渲染的流程图](images/48-使用tab栏按钮实现界面切换时页面渲染的流程图.jpeg) **组件基本功能相关图件:** ### 5.项目数量栏功能逻辑图 ![49-项目数量栏功能逻辑图](images/49-项目数量栏功能逻辑图.jpeg) ### 6.tab选项卡功能逻辑图 ![50-tab选项卡功能逻辑图](images/50-tab选项卡功能逻辑图.jpeg) ### 7.批量删除功能逻辑图 ![51-批量删除功能逻辑图](images/51-批量删除功能逻辑图.jpeg) ### 8.tab选项卡功能最终效果图 ![52-实现todoinfo组件-tab栏效果图](images/52-实现todoinfo组件-tab栏效果图.gif) ### 9.批量删除功能最终效果图 ![53-实现todoinfo组件-批量删除按钮效果图](images/53-实现todoinfo组件-批量删除按钮效果图.gif) # 十四、实现全选功能 ## (一)描述 设计一个全选/全不选按钮,它需要实现以下效果: 1. 点击按钮后所有项目都呈现被选中的效果,如果点击时所有项目已经被选中,则呈现全不选效果 2. 点击按钮实现全选效果时,更新数据库使,所有项目的`completed`属性为1,反之,所有项目的`completed`属性为0 3. 只要视图中的项目全部处于选中状态(无论是通过全选按钮还是勾选按钮又或是其他方式),全选按钮都会高亮显示,反之,全选按钮不会高亮 效果示意图: ![54-实现全选全不选按钮-全选按钮效果示意图](images/54-实现全选全不选按钮-全选按钮效果示意图.gif) ## (二)思路 ### 1.在`MainTodo`组件的模板中添加一个按钮 这里的按钮其实有多种实现方式,比如说使用`button`标签、`input`标签、`a`标签等。 我在这里借鉴了Evan You的写法,使用` +