脚手架 + 指令

发布于:2025-04-02 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、脚手架

1. Vue代码开发方式

Vue有2种开发方式

1.1 传统开发模式

传统开发模式: 直接在HTML文件中, 通过<script src="vue.js"></script>

目录结构:

Vue-tradition-way

        index.html

        index.css

        index.js

        vue.js

优点: 简单, 上手快

缺点: 功能单一(难以使用模块导入导出), 开发体验差

1.2 现代(工程化)开发模式

工程化开发模式: 需要基于Vite/Webpack等进行开发, 这种方式是企业采用的方式, 之后学习都采用这种方式. 

目录结构:

优点: 功能全面, 开发体验好...(在这种环境下, 我们修改代码, 不需要手动刷新, 浏览器会自动帮我们刷新)

缺点: 目录结构复杂, 看上去不易上手, 提高了理解难度

2. 准备工程化环境

1> 安装Nodejs

去官方下载: Node.js — Run JavaScript Everywhere

node -v npm-v 检测node 和 npm是否安装

2> npm 换源: 建议使用淘宝源,下载速度快:npm config set registry https://registry.npmmirror.com

3> 其他的包管理器

        npm

        yarn

        pnpm

4> 三个包管理器使用的命令

npm装包: npm i 包名     yarn装包: yarn add 包名       pnpm装包: pnpm i 包名

npm删包: npm un 包名  yarn删包: yarn remove 包名 pnpm删包: pnpm un 包名

3. 创建Vue工程化项目

3.1 Vue3脚手架

脚手架: 最早来源于建筑工地, 是一个可以保证各项工作顺利开展的平台, 是基础. 基于这个环境, 可以做更多的工作.

Vue3的脚手架: 提供了一个编写Vue3代码的的工程化环境, 基于这个环境, 快速学习或者开发Vue项目. 

好处: 拿来就用, 零配置, 降低Vue3的学习门槛, 让我们更加关注Vue3的语法, 而不必过多在意环境的搭建.

3.2 创建项目

步骤

1. 选定代码的存放位置, 比如D盘

2. 在选定的位置处, 打开命令行窗口: 输入cmd点击回车, 执行命令行`npm create

vue@latest',跟随提示进行后续操作

3. 进入项目根目录

4. 安装项目依赖

5. 启动项目

6. 浏览器运行, 观察效果

4. 认识脚手架目录及文件

目录和文件的解读

src下面写源码, 使用vite打包工具,把vue,css,图片进行打包成浏览器认识的html,css,js,然后交给index.html通过浏览器呈现在我们面前

5. 分析3个入口文件的关系

三个入口文件: main.js(整个项目的打包入口), App.vue(Vue代码的入口),index.thml(项目的入口网页)

mian.js :整个项目的打包入口

App.vue : Vue代码的入口(欢迎界面)

图解: 基本上就是src里面的main.js引用了其他的文件,其他文件又引用了别的文件, 然后就一起被vite打包了,然后处理成css,js,img,最后交给index.html,当浏览器访问5173端口号的时候,就是访问整个index.html文件. 打包前很多的vue文件浏览器不能识别, 必须通过vite来进行处理(vue文件->网页可以识别的文件) mian.js是架起vue代码和网页代码的一座桥梁,它通过对vue代码进行打包,打包后变成js,css,img, 才可以被网页运行起来.

6. Vue单文件和清理目录结构

Vue单文件介绍

引入

代码写⼀起、担⼼class类名、js变量名 重名冲突?Vue中如何避免呢?

此时就要引入Vue单文件了

Vue单文件(一个.vue位结尾的文件)

1. Vue推荐采用.vue文件进行Vue代码的开发

2. 一个Vue单文件的组成: 3部分 = script(JS) + template(HTML)+ style(CSS)

3. 好处: 一个.vue文件时一个独立的模块, 就是一个独立的作用域, 无需担心变量重名

4. style 里面有个scoped属性(当前的style样式只针对当前的template里面的html标签生效,不会影响到别的.vue里面的标签样式), 是为了避免css样式冲突.

5.  Vue 文件浏览器不能识别, 需要借助 vite 进行打包, 打包成 html,css,js,图片等, 然后通过 index.html 进行访问.

清理目录文件

为了便于后续学习Vue代码,这⾥需要清除默认不需要的⽬录结构

步骤

清理欢迎界面:

1> 删除 assets 目录

2> 删除 components 目录

3> 把 App.vue(Vue代码的入口) 的三部分(js,html,css)清空

4> 清空 main.js , 自己重写编写代码

        1. 导入 createApp

        2. 导入 App.vue

        3. 创建应用并指明渲染的位置

具体操作 

后续我们如果要在页面显示什么标签, 就在template里面写

注意要保证项目启动: npm run dex

整体流程

最后我们再把js和css里面的代码清空, 此时就是空白的界面

然后我们就可以把assets和componets给删除了

最后重写 main.js 这个打包入口文件

7. setup简写+差值+响应式渲染

传统环境使用Vue和工程化环境使用Vue

传统方式渲染 Hello world:

1.  引入 vue3 的源代码

2. 准备渲染容器

3. 创建应用并指定渲染的位置, 同时提供数据并返回

4. 差值进行渲染{{ 表达式 }}       

工程化环境下渲染 Hello world:

1. 在 App.vue中提供 setup 函数, 声明数据并返回

2. 差值渲染 {{表达式}}

 演示过程

简写vs不简写

直接在script 里面写setup属性就可以了

具体代码:

不简写

<!-- js -->
<script>
//默认导出 setup
//setup不简写
export default {
  setup() {
    // 声明数据
    const msg = "Hello Vue3+vite"
    //返回数据
    return {
      msg
    }
  }
}
</script>
<!-- html -->
<template>
  <!-- 差值进行渲染 -->
  <h1>{{ msg }}</h1>
</template>
<!-- css -->
<style scoped></style>

简写

<!-- js -->
<!-- setup简写 -->
<script setup>
//申明变量, 此时不用返回,因为默认给我们返回了
const msg = "Hello"

</script>
<!-- html -->
<template>
  <h1>{{ msg }}</h1>
</template>
<!-- css -->
<style scoped></style>

给数据加上响应式

主要是要要在js里面进行按需导入,引用要用到的响应式函数

<!-- js -->
<!-- setup简写 -->
<script setup>
// 按需导入响应式数据 ref,reactive
//申明变量, 此时不用返回,因为默认给我们返回了
import { ref, reactive } from 'vue'
//创建一个 ref 响应式数据
const msg1 = ref("Hello 小白")
//创建一个 reactive响应式对象
const obj = reactive({
  name: "xiaobai",
  age: 19
})
//创建函数
const fn = () => {
  return 90
}
</script>
<!-- html -->
<template>
  <!-- 1. 直接放变量 -->
  <h1>{{ msg1 }}</h1>
  <!-- 2. 对象.属性 -->
  <h1>我是{{ obj.name }}今年{{ obj.age }}岁了</h1>
  <!-- 3. 三元表达式 -->
  <h1>{{ obj.age > 18 ? '成年' : '未成年' }}</h1>
  <!--4. 算数运算 -->
  <h1>明年{{ obj.age + 1 }}岁</h1>
  <!-- 5.函数的调用 -->
  <h1>我考了{{ fn() }}分</h1>
  <!-- 6. 方法的调用 -->
  <h1>{{ msg1 }}有{{ msg1.split(' ').length }}个单词</h1>
</template>
<!-- css -->
<style scoped></style>

运行结果

安装插件快速生成script,template,style标签

为了今后可以快速⽣成 vue ⽂件的三部分组成,建议安装VSCode的插件 Vue3 Snippets, 然后在 vue ⽂件中输⼊ vbase 即可快速⽣成模版

二、指令

1. 指令介绍

指令的概念, 作用, 分类

指令: 

1. 概念: 英文叫做 directive, 作用在标签上, 以 v- 开头的属性(指令就是一个作用在标签上的属性), 是vue 的一种特殊语法.

2. 作用: 增强标签渲染数据的能力

3. 分类: 

1> 内容渲染指令: 作用类似于差值表达式, 把表达式的结果渲染到双标签中.

2> 属性绑定指令: 把表达式的值和标签的属性动态绑定.(标签内容变了, 属性也会进行变化).

3> 事件绑定指令: 用来与标签进行事件绑定, 处理用户交互.

4> 条件渲染指令: 根据表达式的true或false, 决定标签是否展示.

5> 列表渲染指令: 基于数组循环生成一套列表.

6> 双向绑定指令: 数据 < - > 视图 (数据和视图相互影响)

2. 内容渲染指令: v-html/v-text

内容渲染指令的作用类似于差值表达式, 把表达式的结果渲染到双标签中.

v-html = "表达式": 会解析标签

v-text = "表达式": 不会解析标签(相当于差值表达式)    

<!-- vbase快速生成模板结构 -->

<script setup>
import { ref } from 'vue'
const str = ref('<span style = "color:red">Hello Vue3</span>')
</script>
<template>
  <div>
    <!-- 会解析span标签 -->
    <p v-html="str">

    </p>
    <!-- 不会解析span标签,直接当作一个字符串处理 -->
    <!-- 和差值没有区别 -->
    <p v-text="str">

    </p>
    <!-- 差值表示 -->
    <p>{{ str }}</p>
  </div>
</template>

<style scoped></style>

具体的执行流程

3. 属性绑定指令: v-bind

差值表达式是不能作用于标签属性的渲染的, 它只能用于双标签内容的展示

为了把 vue表达式结果 标签 的属性动态绑定,我们引入属性绑定指令v-bind

语法: v-bind:属性名 = "表达式"

简写: :属性名 = "表达式" 注意: 冒号一定不能丢!

<script setup>
import { ref } from 'vue'
const msg = ref('hello vue')
const url = ref('http://www.baidu.com')
const imgurl = ref('https://bkimg.cdn.bcebos.com/pic/f31fbe096b63f6246b60d38cc81dfcf81a4c500f05b0?x-bce-process=image/format,f_auto/quality,Q_70/resize,m_lfit,limit_1,w_536')
</script>
<template>
  <!-- v-bind 不简写 -->
  <div v-bind:title="msg">
    {{ msg }}
  </div>
    <!-- v-bind 简写 -->
  <div :title="msg">
  </div>
  <!-- 练习 百度一下,加冒号 -->
  <a :href="url">百度一下</a>
  <!-- 练习 百度一下,不加冒号 -->
  <a href="url">百度一下</a>
  <!-- 练习 放置网络图片 -->
   <br>
   <img :src="imgurl">
</template>


<style scoped></style>

这样在HTML里面我们就写的会很简洁, 内容都可以放到js里面 

4. 事件绑定指令: v-on

1. 作用: 与DOM元素进行事件绑定/处理.(标签进行事件绑定, 处理用户交互.)

2. 语法(3种)

v-on:事件名="三种写法" 这里的事件是大都是js原生的事件 如: click()点击事件

1> v-on:事件名="内联代码"

2> v-on:事件名="处理函数"

3> v-on事件名="处理函数(实参列表)"

3. 简写: v-on:简写为@

@事件名="处理函数"

<script setup>
// 按需导出
import { ref } from 'vue'
// 计数器
const count = ref(0)
// 自增函数(无参函数)
//ref修饰的变量在js使用需要.value
const increase = () => {
  count.value++;
}
//有参函数
const add = (num) => {
  count.value += num
}

</script>
<template>
  <div>
    <p>{{ count }}</p>

    <!-- 1. 内敛/行内代码(一句话的时候) -->
    <button v-on:click="count++">+1</button>
    <!-- 2. 调用无参函数 -->
    <button v-on:click="increase">+1</button>
    <!-- 3. 调用有参函数 -->
    <button v-on:click="add(1)">+1</button>
    <button v-on:click="add(5)">+5</button>
    <!-- 简写 -->
    <button @click="add(5)">+10</button>

  </div>
</template>

<style scoped></style>

5. 条件绑定指令: v-show / v-if/v-else

1. 作用: 根据vue表达式值是true还是false, 决定某个元素是显示还是隐藏

2. 语法: 

v-show="布尔表达式"   原理: 是控制style里面的display属性设置是否隐藏的

v-if="布尔表达式"

如果表达式是true, 则盒子都显示; 否则,盒子都隐藏

 先看这么一个例子, 我们设置在vue开发者工具v-show,v-if为false

v-show修饰的标签多了style属性,且设置为display, 而v-if修饰的标签直接被注释掉了

3. 区别:

控制元素显示或者隐藏的原理不同:

1> v-show: 是通过控制元素css的display属性控制元素显示或者隐藏.(true->显示, false->display属性设置为none

2> v-if: 是通过创建/插入元素或者移除DOM元素控制显示或者隐藏.(true->插入DOM元素, false->移除DOM元素)

4. 如何选择?

如果频繁的控制元素显示或者隐藏, 推荐v-show(这个元素始终在网页上面,v-if会一直创建,销毁元素这样的话会使得DOM数的开销性能变大); 如果并不频繁控制元素显示或者隐藏, 推荐v-if

5. v-if还可以配合v-else-if和v-else做多分支或多分支的条件渲染

具体代码

<!-- 决定元素是隐藏还是显示 -->
<script setup>
import { ref } from 'vue'
// 控制盒子是否可见
const visible = ref(true)
// 模拟是否登录
const isLogin = ref(true)
//成绩
const mark = ref(100)
</script>
<template>
  <div>
    <!-- v-show -->
    <div class="red" v-show="visible"></div>
    <!-- v-if -->
    <div class="green" v-if="visible"></div>
    <hr/>
    <!-- v-if配合v-else做双分支渲染 -->
    <!-- 需求: 登录显示xxx,欢迎回来, 没登陆显示你好请登录 -->
    <div v-if="isLogin">xxx,欢迎回来</div>
    <!-- v-else 是不需要加条件的-->
    <div v-else>你好,请登录</div>
    <hr/>>
    <!-- v-if配合v-else-if和v-else做多分支渲染 -->
     <!-- 判断成绩是优秀,良好,差 -->
      <div v-if="mark>=90">优秀</div>
      <div v-else-if="mark>=70">良好</div>
      <div v-else>差</div>
  </div>
</template>

<style scoped>
.red,
.green {
  width: 200px;
  height: 100px;
}

.red {
  background: red;
}

.green {
  background: green;
}
</style>

运行结果

6. 小案例1: 切换图片

效果:

需求: 

默认展示数组中的第⼀张图片,点击上⼀页下⼀页来回切换数组中的图片 

1. 要用到弹性布局(设置为fex后,弹性子元素通常在弹性盒子内一行显示。默认情况每个容器只有一行),在父元素定义flex,子元素也要定义flex

2. 实现功能

1> 准备图片路径数组

2> 准备一个响应式下标数据(默认值为0)

3> 给按钮添加点击事件 每次点击会触发index的增大或者减小.然后从而转化img的图片

4> 让下标自增或自减

5> 给按钮添加条件渲染指令 处理边界 

具体代码

<script setup>
import { ref } from 'vue'
//1. 声明图片路径数组
const imgList = [
  'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-01.gif',
  'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-02.gif',
  'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-03.gif',
  'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-04.png',
  'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-05.png']
//2. 声明响应式下标,初始值为0
const index = ref(0)
</script>
<template>
  <div>
    <!-- 添加点击事件click,每次点击,index-1 -->
    <!-- 添加条件渲染, 控制下标的边界 -->
    <button @click="index--" v-if="index > 0">上一页</button>
  </div>
  <!-- v-bind 不简写 -->
  <!-- <img v-bind:src="imgList[index]" alt="img"> -->
  <!-- 简写 -->
  <img :src="imgList[index]" alt="img">
  <div>
    <!-- 添加点击事件click,每次点击,index+1 -->
    <!-- 添加条件渲染, 控制下标的边界 -->
    <button @click="index++" v-if="index < imgList.length
      - 1">下一页</button>
  </div>
</template>

<style>
#app {
  /* 定义为弹性盒子,展现在一行 */
  display: flex;
  width: 500px;
  height: 240px;
}

/*  */
img {
  width: 240px;
  height: 240px;
}

#app div {
  flex: 1;
  /**使得 #app 内的每个 div 元素在容器中占据同等的宽度,分配的宽度是剩余空间的平分。 */
  display: flex;
  /**每一个盒子都设置为弹性布局 */
  justify-content: center;
  align-items: center;
}
</style>

7. 小案例2: 可折叠面板

可折叠面版: 我们需要的效果图

我们要的效果是, 点击收起,下面的文字就会被折叠

步骤:

1. 搭建HTML框架

2. 准备一个响应式的布尔数据(按钮和盒子的状态有俩种,要么展开要么收起来,下面的盒子显示就设置为true, 不显示就设置为false, 每次点击收起, 就会改变布尔值)

3. 通过 v-show 绑定布尔值控制盒子的显示或隐藏

4. 给按钮绑定点击事件, 每次点击就让布尔值取反

5. 布尔值控制按钮名称

具体代码 

<script setup>
import { ref } from 'vue'
// 是否可见, 默认不可见, 初始值是 false
const visible = ref(false)
</script>

<template>
	<!-- 面板区域 -->
	<h3>可折叠面板</h3>
	<div class="pane1">
		<!-- 标题区域 -->
		<div class="title">
			<h4>自由与爱情</h4>
			<!-- 每次点击就取反, 并且按钮的显示信息也会跟着改变 -->
			<span class="btn" v-on:click="visible = !visible">{{ visible === false ? '展开' : '收起' }}</span>
		</div>

		<div>
			<!-- 主体内容区域 -->
			<!-- 默认为显示不可见 -->
			<div class="container" v-show="visible">
				<p>声明诚可贵</p>
				<p>爱情价更高</p>
				<P>若为自由故</P>
				<p>俩者皆可抛</p>
			</div>
		</div>
	</div>


</template>


<style lang="scss">
// 如果lang = "scss"设置, 项目运行不出来, 需要安装sass模块, 在cmd里面运行 npm i sass -D,再重写运行项目: npm run dev
/* 背景色 */
body {
	background: #ddd;
}

#app {
	width: 400px;
	margin: 20px auto;
	padding: 1em 2em 2em;
	border: 4px solid green;
	box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
	background: #fff;
	border-radius: 1em;

	h3 {
		/* css不支持渲染嵌套的写法因此我们使用scss */
		// 让h3水平居中
		text-align: center;
	}
}

.pane1 {
	.title {
		display: flex;
		justify-content: space-between;
		align-items: center;
		border: 1px solid #ccc;
		padding: 0 1em;

		h4 {
			margin: 0;
			line-height: 2;
		}

		.btn {
			cursor: pointer; //鼠标移动到上面之后, 变成手形
		}

	}

	.container {
		border: 1px solid #ccc;
		padding: 0 1em;
		border-top-color: transparent;
	}
}
</style>

注意: 如果lang = "scss"设置, 项目运行不出来, 需要安装sass模块, 在cmd里面运行 npm i sass -D,再重写运行项目: npm run dev

css不支持渲染嵌套的写法因此我们使用scss

8. 列表渲染指令: v-for

1. 作用: 基于数组/对象/数字 循环生产列表

2. 语法:

1> 想循环谁, 就给谁添加v-for

2> <li v-for="(值变量,下标变量) in 数组"></li> 值变量是当前的数组元素, 下标变量是数组的下标

<script setup>
import { ref } from 'vue'
// 遍历数字数组
const nums = ref([11, 22, 33, 44])
// 遍历对象数组,商品列表
const goodsList = ref([
	{ id: 1, name: '篮球', price: 100 },
	{ id: 2, name: '乒乓球', price: 10 },
	{ id: 3, name: '排球', price: 60 }
])
// 遍历对象
const obj = {
	id: 1000,
	name: 'xxiao',
	age: 90

}
</script>
<template>
	<div>
		<!-- 根据数组的元素生成四个li分别包含11,22,33,44 -->
		<ul>
			<li v-for="(item, index) in nums">
				<!-- 遍历数字数组 -->
				{{ item }}=>{{ index }}
			</li>
		</ul>

		<!-- 根据goods-list的对象生成 div-->
		<!-- 遍历对象数组 -->
		<div class="goods-list">
			<div class="goods-item" v-for="(item) in goodsList">
				<p>id ={{ item.id }}</p>
				<p>name={{ item.name }}</p>
				<p>price={{ item.price }}</p>
			</div>
		</div>
		<!-- 遍历对象 -->
		<!--  -->
		<div class="obj">
			<!-- key是对象的键,value是对象的值,index是对象的下标 -->
			<div class="obj-item" v-for="(key, value, index) in obj">
				<p>{{ key }} : {{ value }} =>{{ index }}</p>
			</div>
		</div>
		<ul>
			<!-- 遍历数字 1-5 -->
			<li v-for="(item,index) in 5">
				{{ item }} -> {{ index }}
			</li>
		</ul>
	</div>
</template>

<style scoped></style>

9. 案例三: 书架

 需求

1.根据左侧数据渲染出右侧列表(v-for)

2.点击删除按钮时,应该把当前⾏从列表中删除(获取当前⾏的index,利⽤splice删除)

具体代码 

<script setup>
import { ref } from 'vue'
const bookList = ref([
	{ id: 1, name: '《红楼梦》', author: '曹雪芹' },
	{ id: 2, name: '《西游记》', author: '吴承恩' },
	{ id: 3, name: '《⽔浒传》', author: '施耐庵' },
	{ id: 4, name: '《三国演义》', author: '罗贯中' }
])
//删除->任意位置删除splice
const onDel = (i) => {
	// console.log(i) i为当前点击下标
	// 删除前先确认
	if (window.confirm('确认删除吗')) {
		bookList.value.splice(i, 1);//从i下标开始删,删除1个
	}
}
</script>
<template>
	<h3>我的书架</h3>
	<ul>
		<li v-for="(item, index) in bookList">
			<!-- 基于这个模板循环生成li-->
			<span>{{ item.name }}</span>
			<span>{{ item.author }}</span>
			<!-- 给按钮绑定点击事件 -->
			<button @click="onDel(index)">删除</button>
		</li>
	</ul>
</template>



<style>
#app {
	width: 400px;
	margin: 100px auto;
}

ul {
	/* 去除小圆点 */
	list-style: none;
}

ul li {
	display: flex;
	justify-content: space-around;
	padding: 10px 0;
	border-bottom: 1px solid #ccc;
}

h3 {

	text-align: center;
}
</style>

10. v-for中的key

添加 key 属性 

1> 作用: 提高 vue 在更新列表的更新性能

2> 语法:  :key = "不重复的唯一值"

3> 原理: vue内部会尽可能的复用DOM,不去创建新的或者更新DOM,做最小的更新. 加了key且为id(不重复的唯一值),通过key来标明当前元素是否发生了变化, 如果key不变, vue直接复用之前的DOM做最小的更新.(key好比对这个元素进行了标记, 改了数据后, vue就会根据这个数据去生成新的DOM结构,然后和原来的DOM进行对比,然后决定哪里更新,会尽可能的去复用上一次的DOM,而不去创建新的.此时我们如果删除id=0的书, 其他1-4的书不变,那么1-4的书不会改变,只有id=0的书做更改)

4> key 的类型: 数字字符串

5> key的选择: 首选 id, 其次用下标(下标会变化一般不使用)

无key的情况下删除元素, 发现影响了很多个地方

加了key之后,发现只影响了一个地方

11. 双向绑定指令: v-model

1. 双向: 数据和视图

1> 当数据变了, 视图会变化(数据驱动视图的思想)

2> 当视图遍历, 数据会变化

数据 <-> 视图

2. 作用: 经常用在表单元素上, 比如输入框, 下拉列表, 单选框, 复选框, 文本域等.用于实现数据和标签vule属性的双向绑定, 进而可以快速收集表单数据.

3. 语法: v-model="表达式"  

具体代码:

<script setup>
import { reactive } from 'vue';
//登录表单
const loginForm = reactive({
	username: '',
	password: ''
})
</script>
<template>
	<div>
		<!-- 实现双向绑定 -->
		账号:<input type="text" v-model="loginForm.username"><br><br>
		密码:<input type="password" v-model="loginForm.password"><br><br>
		<button>登录</button>
		<button>重置</button>
	</div>
</template>



<style scoped></style>

12. 案例四: 记事本

需求 

1> 能够在输入框输入代办事项

2> 能够添加待办任务

3> 能够删除待办任务

4> 能够清空待办任务

5> 能够给待办任务计数

主要的搭建框架

添加待办事项分析

本质: 往数组里面添加了一个对象

部分代码:

删除待办事项:

本质 : 获取下标, 进行删除

部分代码:

合计和清空

本质: 合计本身就是数数组有多少个元素,我们直接就使用数组长度来代替.清空直接让数组指向一个空数组即可.

部分代码

完整代码 

<script setup>
// 导⼊ todo 样式
import './styles/index.css'
import { ref } from 'vue'
// 代办任务列表
const todoList = ref([
	{ id: 321, name: '吃饭', finished: false },
	{ id: 666, name: '睡觉', finished: true },
	{ id: 195, name: '打⾖⾖', finished: false }
])
//任务名称
const title = ref('')
//添加
const onAdd = () => {
	// 去除title的首位空格
	const name = title.value.trim()
	// 非空校验
	// if(name.length === 0)
	if (!name) {//name有值为false,无值为true
		return alert('名称不能为空')
	}
	// 给 todoList 末尾添加一个新对象
	todoList.value.push({
		// 使用时间戳生成id(时间的毫秒数)
		// 获取时间戳的三种方式
		// Date.now()
		// new Date().geTime()
		// +new Date()
		id: Date.now(),//时间戳
		name: name,//任务名称
		finished: false//刚加进去都是false
	})
	// 清空输入框
	title.value = ''
}
//删除
const onDel = (i) => {
	// 删除前的确认提示
	if (confirm('确认删除?')) {//点击确认就进入if
		// 调用 splice() 从当前点击的位置删除1个
		todoList.value.splice(i, 1);
	}
}
//清空
const onClear = () => {
	if (confirm('确认清空?')) {
		todoList.value = [];
	}
}
</script>
<template>
	<!-- section相当于div -->
	<section clsss="todoapp">
		<!-- 头部 -->
		<header class="header">
			<h1>记事本</h1>
			<!-- 双向绑定,输入的数据<->视图 收集用户输入的值 -->
			<input placeholder="请输⼊任务" class="new-todo" v-model="title" />
			<!-- 把上面的值加在任务待办表单里面 -->
			<button class="add" v-on:click="onAdd">添加任务</button>
		</header>
		<!-- 主体 -->
		<section class="main">
			<ul class="todo-list">
				<!-- 循环生成li 也就是任务待办事项-->
				<li class="todo" v-for="(item, index) in todoList" :key="item.id">
					<div class="view">
						<!-- 序号+待办事项 -->
						<span class="index">{{ index + 1 }}</span>
						<label>{{ item.name }}</label>
						<!-- 点击×就进行删除 -->
						<button class="destroy" @click="onDel(index)"></button>
					</div>
				</li>
			</ul>
		</section>
		<!-- 尾部 -->
		<footer class="footer">
			<span class="todo-count">合 计: <strong> {{ todoList.length }} </strong></span>
			<button class="clear-completed" @click="onClear()">清空任务</button>
		</footer>
	</section>
</template>

我们样式放在src里面style里面

html,
body {
    margin: 0;
    padding: 0;
}

body {
    background: #fff;
}

button {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 100%;
    vertical-align: baseline;
    font-family: inherit;
    font-weight: inherit;
    color: inherit;
    -webkit-appearance: none;
    appearance: none;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

body {
    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    line-height: 1.4em;
    background: #f5f5f5;
    color: #4d4d4d;
    min-width: 230px;
    max-width: 550px;
    margin: 0 auto;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-weight: 300;
}

:focus {
    outline: 0;
}

.hidden {
    display: none;
}

#app {
    background: #fff;
    margin: 180px 0 40px 0;
    padding: 15px;
    position: relative;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

#app .header input {
    border: 2px solid rgba(175, 47, 47, 0.8);
    border-radius: 10px;
}

#app .add {
    position: absolute;
    right: 15px;
    top: 15px;
    height: 68px;
    width: 140px;
    text-align: center;
    background-color: rgba(175, 47, 47, 0.8);
    color: #fff;
    cursor: pointer;
    font-size: 18px;
    border-radius: 0 10px 10px 0;
}

#app input::-webkit-input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}

#app input::-moz-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}

#app input::input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: gray;
}

#app h1 {
    position: absolute;
    top: -120px;
    width: 100%;
    left: 50%;
    transform: translateX(-50%);
    font-size: 60px;
    font-weight: 100;
    text-align: center;
    color: rgba(175, 47, 47, 0.8);
    -webkit-text-rendering: optimizeLegibility;
    -moz-text-rendering: optimizeLegibility;
    text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
    position: relative;
    margin: 0;
    width: 100%;
    font-size: 24px;
    font-family: inherit;
    font-weight: inherit;
    line-height: 1.4em;
    border: 0;
    color: inherit;
    padding: 6px;
    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.new-todo {
    padding: 16px;
    border: none;
    background: rgba(0, 0, 0, 0.003);
    box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}

.main {
    position: relative;
    z-index: 2;
}

.todo-list {
    margin: 0;
    padding: 0;
    list-style: none;
    overflow: hidden;
}

.todo-list li {
    position: relative;
    font-size: 24px;
    height: 60px;
    box-sizing: border-box;
    border-bottom: 1px solid #e6e6e6;
}

.todo-list li:last-child {
    border-bottom: none;
}

.todo-list .view .index {
    position: absolute;
    color: gray;
    left: 10px;
    top: 20px;
    font-size: 22px;
}

.todo-list li .toggle {
    text-align: center;
    width: 40px;
    /* auto, since non-WebKit browsers doesn't support input styling */
    height: auto;
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    border: none;
    /* Mobile Safari */
    -webkit-appearance: none;
    appearance: none;
}

.todo-list li .toggle {
    opacity: 0;
}

.todo-list li .toggle+label {
    /*
    Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
    IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
  */
    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
    background-repeat: no-repeat;
    background-position: center left;
}

.todo-list li .toggle:checked+label {
    background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}

.todo-list li label {
    word-break: break-all;
    padding: 15px 15px 15px 60px;
    display: block;
    line-height: 1.2;
    transition: color 0.4s;
}

.todo-list li.completed label {
    color: #d9d9d9;
    text-decoration: line-through;
}

.todo-list li .destroy {
    display: none;
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    margin: auto 0;
    font-size: 30px;
    color: #cc9a9a;
    margin-bottom: 11px;
    transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
    color: #af5b5e;
}

.todo-list li .destroy:after {
    content: '×';
}

.todo-list li:hover .destroy {
    display: block;
}

.todo-list li .edit {
    display: none;
}

.todo-list li.editing:last-child {
    margin-bottom: -1px;
}

.footer {
    color: #777;
    padding: 10px 15px;
    height: 20px;
    text-align: center;
    border-top: 1px solid #e6e6e6;
}

.footer:before {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 50px;
    overflow: hidden;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
        0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
        0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
    float: left;
    text-align: left;
}

.todo-count strong {
    font-weight: 300;
}

.filters {
    margin: 0;
    padding: 0;
    list-style: none;
    position: absolute;
    right: 0;
    left: 0;
}

.filters li {
    display: inline;
}

.filters li a {
    color: inherit;
    margin: 3px;
    padding: 3px 7px;
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: 3px;
}

.filters li a:hover {
    border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
    border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
    float: right;
    position: relative;
    line-height: 20px;
    text-decoration: none;
    cursor: pointer;
}

.clear-completed:hover {
    text-decoration: underline;
}

.info {
    margin: 50px auto 0;
    color: #bfbfbf;
    font-size: 15px;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
    text-align: center;
}

.info p {
    line-height: 1;
}

.info a {
    color: inherit;
    text-decoration: none;
    font-weight: 400;
}

.info a:hover {
    text-decoration: underline;
}

/*
  Hack to remove background from Mobile Safari.
  Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {

    .toggle-all,
    .todo-list li .toggle {
        background: none;
    }

    .todo-list li .toggle {
        height: 40px;
    }
}

@media (max-width: 430px) {
    .footer {
        height: 50px;
    }

    .filters {
        bottom: 10px;
    }
}