一、脚手架
脚手架:一个保证各项工作顺利开展的平台,方便我们 拿来就用,零配置
1. Vue 代码开发方式
相比直接 script 引入 vue 源码,有没有更好的方式编写vue代码呢?
① 传统开发模式:
- 基于html文件开发Vue,类似jQuery的使用
<script src="vue.js"></script>
- 优点:简单、上手快
- 缺点:功能单一、开发体验差
② 工厂化开发模式:
在 构建工具(Vite/Webpack )环境下开发Vue,这是最推荐的、也是企业采用的方式
- 优点:功能全面,开发体验好
- 缺点:目录结构复杂,理解难度提升
2. 准备工程化环境
① 安装工具 Nodejs
注意: 安装18.3或更高版本,Nodejs 官网:https://nodejs.org/en/
安装好之后,可以打开命令行,输入下面指令,进行测试如下:
> node -v
v22.13.1> npm -v
10.9.2
npm 换源 – 避免下载过慢,当前下载好的可以不用管
// 查看 npm 源
npm config get registry
// 默认是指向 https://registry.npmjs.org/,也就是官⽅源
// 为了提⾼npm下载速度, 可以给npm换源
// 国内源有很多,我这⾥⽤淘宝源吧。毕竟是⼤公司,会⽐较稳定
npm config set registry https://registry.npmmirror.com
// 再⼀次查看 npm 源
npm config get registry
② 安装 yarn 和 pnpm
yarn和 pnpm、还有 npm
三者的功能类似,都是包管理工具, 用来下载或删除模块包,性能上 yarn
和 pnpm
优于 npm
命令 | 装包 | 删包 |
---|---|---|
npm | npm i 包名 | npm un 包名 |
yarn | yarn add 包名 | yarn remove 包名 |
pnpm | pnpm i 包名 | pnpm un 包名 |
在命令行上进行安装,如下:
# windows系统
npm install yarn -g
npm install pnpm -g
___________________________________
# mac系统
sudo npm install yarn -g
sudo npm install pnpm -g# 检测是否安装成功, 如下
> yarn -v
1.22.22> pnpm -v
10.6.4
3. 创建 Vue 工厂化项目
创建步骤如下:
- 选定⼀个存放位置,比如选择桌面,根据自己情况,选择D盘或E盘等
- 执行命令
npm create vue@latest
,会安装并执行create-vue
, 它是Vue官方的项目脚手架工具 - 进⼊项目根目录:
cd
项目名称 - 安装 vue 等模块依赖:
npm i
- 启动项目:
npm run dev
,会开启⼀个本地服务器,然后在浏览器网址栏输入:http://localhost:5173
C:\Users\>cd desktop # 先切换到桌面C:\Users\Desktop>npm create vue@latest
Need to install the following packages:
create-vue@3.15.1
Ok to proceed? (y) y> npx
> create-vueT Vue.js - The Progressive JavaScript Framework
|
o 请输入项目名称:
| vue-engineering-way
|
o 请选择要包含的功能: (↑/↓ 切换,空格选择,a 全选,回车确认)
| Prettier(代码格式化)
安装选项如下:
启动服务如下:
\Desktop\vue-engineering-way> npm run dev> vue-engineering-way@0.0.0 dev
> viteVITE v6.2.2 ready in 1172 ms➜ Local: http://localhost:5173/➜ Network: use --host to expose➜ Vue DevTools: Open http://localhost:5173/__devtools__/ as a separate window➜ Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools➜ press h + enter to show help
最后呈现的界面效果如下,说明项目创建并启动成功了
补充 – 后继我们要打开这个界面,就需要先运行,然后输入 localhost:端口号(看自己设定的端口号是多少,我这里是 5173)
4. 认识工程化项目下目录和文件
用 vscode 打开查看,如下:
我们今后Vue代码写哪个目录下?
- 答: src 目录,src下的所有代码会被
vite
打包成css/js/img
, 然后交给index.html
,最终通过浏览器呈现在用户眼前
分析上面三个入口文件关系:
1、main.js、App.vue、index.html三个文件的作用?
- main.js - 项目打包的入口 - 创建应用
- App.vue - Vue代码的入口(根组件)
- index.html- 项目的入口网页
2、mian.js、App.vue、index.html 三者的关系是什么?
- App.vue(vue入口)=>main.js(项目打包入口)index.html(浏览器入口)
- main.js 是 Vue 代码通向网页代码的桥梁,非常关键
5. Vue 单文件
思考:代码写一起,会不会出现class类名、js变量名 重名冲突?Vue中如何避免呢?
vue单文件介绍
- vue推荐采用
.vue
的文件来开发项目 - 一个 vue 文件通常有3部分组成,
script(JS)+template(HTML)+ style(CSS)
- 一个 vue 文件是 独立的模块,作用域互不影响
- style 配合
scoped
属性,保证样式只针对当前 template 内的标签生效
作用:提供了独立的作用域,不用担心 JS 变量名、CSS 选择器名冲突
注意:.vue 文件浏览器无法识别,需要借助 vite打包成 js、css 等,最终交给 index.html
,通过浏览器呈现效果
6. 清理目录结构
- 删除assets文件夹
- 删除components文件夹
- 清除App.vue的内容
- 清除main.js的内容
补充内容
App.vue
<script setup></script>
<template>App根组件
</template>
<style></style>
main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
7. set up简写 + 插值 + 响应式
实例1:完整写法
<script>export default{setup(){const msg = 'Hello World' // 声明数据return {msg} // 返回数据}}</script><template><h1>{{msg}}</h1>
</template>
实例2:简写
<script setup>const msg = 'Hello World'
</script><template><h1>{{msg}}</h1>
</template>
实例3:练习
<script setup>import { reactive, ref } from 'vue' // 通过导入的方法模块化// 字符串const msg = ref('Hello World') // 对象const obj = reactive({name: 'vue3',age: 18})// 函数function fn(){return 100}
</script><template><h1>{{msg}}</h1><p>{{obj.name}}, 今年{{ obj.age }}岁</p><p>函数返回值:{{ fn() }}</p>
</template>
二、指令
1. 基本概念
指令(Directives)是Vue提供的带有v-前缀的特殊标签属性,用来增强标签的能能力
- 作用:提高标签数据渲染的能力
vue3 中的指令按照不同的用途可以分为如下 6 大类:
- 内容渲染指令(v-html、v-text):作用类似于插值,把表达式的结果渲染到双标签
- 属性绑定指令(v-bind):把表达式的值与标签的属性 动态绑定
- 事件绑定指令(v-on):用来与标签进行事件绑定,处理用户交互
- 条件渲染指令(v-show、v-if、v-else、v-else-if):根据表达式的 true 或 false,决定标签是否展示
- 列表渲染指令(v-for):基于数组循环生成一套列表
- 双向绑定指令(v-model):数据 <–> 视图(数据与视图相互影响,双向奔赴)
2. 内容渲染指令
内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下2个:
v-text
(类似innerText)- 使用语法:
<p v-text="表达式"></p>
,意思是将 表达式的 值渲染到 p标签中 - 类似
innerText
,使用该语法,会覆盖p标签原有内容
- 使用语法:
v-html
(类似innerHTML)- 使用语法:
<p v-html-"表达式"></p>
,意思是将 表达式的 值渲染到p标签中。 - 类似
innerHTML
,使用该语法,会覆盖p标签原有内容,并且能够将HTML标签的样式呈现出来。
- 使用语法:
代码如下:
<script setup>import { reactive, ref } from 'vue' // 通过导入的方法模块化const str = ref('<span style="color: red">Hello Vue</span>')
</script><template><p v-text="str"></p><!-- 上下两个达到的效果是一样的 --><p>{{str}}}</p> <p v-html="str"></p>
</template>
效果如下:
3. 属性绑定指令
作用:把表达式的结果 与标签的属性动态绑定 3
语法:
v-bind: 属性名="表达式" (可简写成 ::属性名="表达式")
基本用法
① 绑定单个属性
使用 v-bind
可以绑定单个属性,例如绑定图片的 src
属性:
<img v-bind:src="imgSrc" alt="">
<!-- 或者使用缩写 -->
<img :src="imgSrc" alt="">
② 绑定多个属性
如果需要绑定多个属性,可以使用对象语法,将多个属性和对应的值放在一个对象中,然后通过 v-bind
绑定这个对象
<img v-bind="{ src: imgSrc, title: imgTitle }" alt="">
③ 绑定 Class
通过 v-bind:class 可以动态绑定元素的 class 属性。例如,根据 isActive 的值动态切换 class:
<div v-bind:class="{ active: isActive }" class="test"></div>
④ 绑定 style
通过 v-bind:style
可以动态绑定元素的 style 属性。需要注意的是,CSS 样式名中的 - 需要转换为驼峰命名法,例如 font-size 需要转换为 fontSize
:
<div v-bind:style="{ background: bground, fontSize: fSize + 'px' }">
hello-vue
</div>
⑤ 传递多个 Props
在父组件向子组件传递多个参数时,可以使用 v-bind 的对象语法,将所有的 props 集中在一个对象中传递:
<child-component v-bind="props"></child-component>
代码样例如下:
<script setup>import { reactive, ref } from 'vue' // 通过导入的方法模块化const url = ref('https://www.baidu.com') const msg = ref('Hello Vue 3')const imgsrc = ref('https://haowallpaper.com/link/common/file/previewFileImg/16677062396530048')
</script><template><p><a v-bind:href="url">百度一下</a></p><!-- 简写 --><p><a :href="url">百度一下</a></p><div v-bind:title="msg">{{ msg }}</div><!-- 绑定多个元素 --><img v-bind="{ src: imgsrc, title: msg }" alt=""><div v-bind:style="{background: 'pink', color: 'red'}">I miss you</div></template>
4. 事件绑定指令
使用Vue时,如需为DOM注册事件,及其的简单,语法如下:
<button v-on:事件名="内联语句">按钮</button>
<button v-on:事件名="处理函数">按钮</button>
<button v-on:事件名="处理函数(实参)">按钮</button>
- 注意:
v-on
可以简写为 @
内联语句指的是直接在HTML标签上使用JavaScript代码的一种方式。在Vue中,可以通过v-on指令将内联语句与DOM事件关联起来,从而在触发事件时执行相应的 JavaScript
代码。
代码示例如下:
<script setup>import { reactive, ref } from 'vue' // 通过导入的方法模块化const cnt = ref(0)// 无参函数const increase = () =>{cnt.value++}// 有参函数const add = (n) =>{cnt.value += n}function increment() {cnt.value++}
</script><template><p>{{ cnt }}</p><!-- 内联/行内代码 --><button @click="cnt++">+1</button><!-- 处理函数 --><button @click="increase">+1</button><!-- 处理函数(实参) --><button @click="add(5)">+5</button><br><button @click="increment">Count is:{{cnt}}</button>
</template>
5. 条件渲染指令
v-show
- 作用:控制元素css 的 display属性来控制元素 显示或隐藏 的
- 语法:v-show=“布尔表达式”【表达式值为 true 显示,false 隐藏】
- 原理:切换
display:none
控制显示隐藏 - 场景:频繁切换显示隐藏的场景
v-if
- 作用:通过创建和插入元素 或移除 DOM 元素 控制元素显示隐藏(条件渲染)
- 语法:
v-if="布尔表达式"
【表达式值 true显示,false 隐藏】 - 原理:基于条件判断,创建 或 移除元素。
- 场景:要么显示,要么隐藏,不频繁切换的场景
v-else 和 v-else-if
- 作用:辅助v-if进行判断渲染
- 语法:
v-else
v-else-if="表达式"
- 需要紧接着v-if使用
代码示例1
<script setup>
import { ref } from 'vue'const awesome = ref(true)function toggle() {awesome.value = !awesome.value
}
</script><template><button @click="toggle">Toggle</button><h1 v-if="awesome">Vue is awesome!</h1><h1 v-else>Oh no 😢</h1>
</template>
- 通过按钮点击就可以切换 文字 显示
代码示例2
<script setup>import { ref } from 'vue'const vis = ref(true) // 是否可见const login = ref(true) // 是否登录const mark = ref(80)
</script><template><!-- v-show --><div class="red" v-show="vis"></div><!-- v-if --><div class="green" v-if="vis"></div><hr><!-- 双分⽀的条件渲染 --> <div v-if="login">xxx, 欢迎回来</div><div v-else>你好, 请登录</div><hr><!--多分⽀的条件渲染: 1. 90及其以上优秀 2. 70到90之间良好 3. 其他的差 --><div v-if="mark >= 90">优秀</div><div v-else-if="mark >= 70">良好</div><div v-else>差</div></template><style scoped>.red, .green{width: 200px;height: 200px;}.red{background-color: red;}.green{background-color: green;}
</style>
6. 列表渲染指令
v-for指令需要使用(item,index)in 目标结构 形式的特殊语法,其中:
item
:数组中的每一项index
:每一项的索引,不需要可以省略- 目标结构:被遍历的 数组/对象/数字
<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: 200 },{ id: 3, name: '排球', price: 300 }])const obj = {id: 10001,name: 'bit',age: 18}
</script><template><div><!-- 遍历数字数组 --><ul><li v-for="(item, index) in nums">{{ item }} =>{{ index }}</li></ul><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><ul><!-- 遍历对象 --> <li v-for="(value, key, index) in obj">{{ value }} => {{ key }} => {{ index }}</li></ul><!-- 遍历数字 --><ul><li v-for="(item, index) in 5">{{ item }} => {{ index }}</li></ul></div></div>
</template><style lang="scss"></style>
v-for 中的 key
语法 :key="唯一值"
- 作用:给列表项添加的唯一标识,便于Vue进行列表项的正确排序复用,因为Vue 的默认行为会尝试原地修改元
素(就地复用)
代码如下:
<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: '施耐庵' }])// 删除function onDel(i){// i: 当前点击下标// 删除前先确认if(window.confirm('确定删除吗?')){// 调用 splice方法删除bookList.value.splice(i, 1)}}
</script><template><h3>书架管理</h3><!-- 无 key --><ul><li v-for="(item, index) in bookList"><span>{{ item.name }}</span><span>{{ item.author }}</span><button @click="onDel(index)">删除</button></li></ul><!-- 有 key 且为 id --><ul><li v-for="(item, index) in bookList" :key="item.id"><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;}
</style>
删除时结果如下:
- 右边闪烁越少,说明 vue 复用性更好,性能也更快
- 因此可以知道:最大限度的复用DOM、从而提⾼DOM的更新性能
注意:
- key 的类型只能是 数字 或 字符串
- key的值必须 唯一,不能重复
- 推荐用 id 作为 key(因为id唯一),不推荐用 index 作为 key( 会变化)
7. 双向绑定指令
所谓双向绑定就是:
- 数据改变 -> 视图变化
- 视图改变 -> 数据变化
作用:在 表单元素(input、select、radio、checkbox)上,实现数据双向绑定。从而可以快速 获取 或设置 表单元素的值
我们可以同时使用 v-bind
和 v-on
来在表单的输入元素上创建双向绑定:
<input :value="text" @input="onInput">
试着在文本框里输入——你会看到 <p>
里的文本也随着你的输入更新了【实时更新】
代码如下:
<script setup>
import { ref } from 'vue'const text = ref('')function onInput(e) {text.value = e.target.value
}
</script><template><input :value="text" @input="onInput" placeholder="Type here"><p>{{ text }}</p>
</template>
为了简化双向绑定,Vue 提供了一个 v-model
指令,它实际上是上述操作的语法糖:
<input v-model="text">
-
v-model
会将被绑定的值与<input>
的值自动同步,这样我们就不必再使用事件处理函数了。 -
v-model
不仅支持文本输入框,也支持诸如多选框、单选框、下拉框之类的输入类型
<script setup>
import { ref } from 'vue'const text = ref('')
</script><template><input v-model="text" placeholder="Type here"><p>{{ text }}</p>
</template>
案例:实现登录界面,需求如下:
- 点击登录按钮获取表单中的内容
- 点击重置按钮清空表单中的内容
<script setup>
import { reactive } from 'vue'// 表单对象
const loginForm = reactive({username: '',password: ''
})// 登录方法
const handleLogin = () => {// 获取表单数据(reactive对象会自动解包,直接使用即可)console.log('提交的表单数据:', {username: loginForm.username,password: loginForm.password})// 这里可以添加实际的登录逻辑,比如调用API
}// 重置方法
const handleReset = () => {// 重置表单字段loginForm.username = ''loginForm.password = ''
}
</script><template><div>账号: <input v-model="loginForm.username" type="text" /> <br/><br/>密码: <input v-model="loginForm.password" type="password" /> <br/><br/><!-- 添加点击事件处理 --><button type="button" @click="handleLogin">登录</button><button type="button" @click="handleReset">重置</button></div>
</template>
三、案例学习
1. 学习之旅
效果如下:
- 需求:默认展示数组中的第⼀张图片,点击上一页下一页来回切换数组中的图片
实现思路
- 数组存储图片路径 [‘url1,url2’url3’
- 准备下标index 去数组中取图片地址
- 通过v-bind给src绑定当前的图片地址
- 点击上一页下一页只需要修改下标的值即可
- 当展示第一张的时候,上一页按钮应该隐藏。展示最后一张的时候,下一页按钮应该隐藏
代码如下:
<script setup>import { ref } from 'vue'// 图片列表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']const i = ref(0)
</script><template><div><button v-if="i >= 1" @click="i--">上一页</button><img :src="imglist[i]" alt="" /> <button v-if="i < imglist.length - 1" @click="i++">下一页</button></div>
</template><style scoped>#app {display: flex;width: 500px;height: 240px;}img {width: 240px;height: 240px;}#app div {flex: 1;display: flex;justify-content: center;align-items: center;}</style>
2. 可折叠面板
效果如下:
实现思路
- 搭建了HTML结构+CSS样式
- 准备一个响应式的布尔数据
- 通过 v-show 绑定布尔值控制盒子的显示或隐藏
- 给按钮绑定点击事件,每点击的时候让布尔值取反
- 布尔值控制按钮的名称
代码如下:
<script setup>import { ref } from 'vue'const visible = ref(true)
</script><template><!-- 面板区域 --><h3>可折叠面板</h3><div class="panel"><!-- 标题区域 --><div class="title"><h4>自由与爱情</h4><span class="btn" @click="visible = !visible"> 收起 </span></div><!-- 主体内容区域 --><div class="container" v-show="visible"><p>生命诚可贵,</p><p>爱情价更高。</p><p>若为自由故,</p><p>两者皆可抛。</p></div></div>
</template><style lang="scss"> body{background-color: #ccc;}#app{width: 400px;margin: 20px auto;padding: 1em 2em 2em;border: 4px solid green;border-radius: 1em;box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);background-color: #fff;}#app h3 {text-align: center;}.panel{.title {display: flex;justify-content: space-between;align-items: center;padding: 0 1em;border: 1px solid #ccc;}.title h4 {line-height: 2;margin: 0;}.container {border: 1px solid #ccc;padding: 0 1em;}.btn {/* ⿏标改成⼿的形状 */ cursor: pointer;}}
</style>
如果上面指明了 lang=“scss” 之后,项目运行失败
- 就需要安装 sass 模块,执行
npm i -D sass
- 然后安装完毕之后,再重新执行项目:npm run dev
3. 书架管理
效果如下:
需求:
- 根据左侧数据渲染出右侧列表(v-for)
- 点击删除按钮时应该把当前行从列表中删除(获取当前行的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: '施耐庵' }])// 删除function onDel(i){// i: 当前点击下标// 删除前先确认if(window.confirm('确定删除吗?')){// 调用 splice方法删除bookList.value.splice(i, 1)}}
</script><template><h3>书架管理</h3><ul><li v-for="(item, index) in bookList"><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;}
</style>
4. 个人记事本
在 src 目录下新建一个 styles目录,然后创建 index.css 文件,如下:
/** @format */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=922433IE 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;}
}
代码如下:
<script setup>
import './styles/index.css'
import { ref } from 'vue'// 任办任务列表
const todoList = ref([{ id: 123, name: '吃饭', finished: false },{ id: 985, name: '睡觉', finished: true },{ id: 666, name: '吃饭', finished: false }
])// 添加任务
const title = ref('')
// 添加
const onAdd = () => {// 去除 title 的首尾空格const name = title.value.trim()// 非空校验if (!name) return alert('名称不能为空')// 可以在 onAdd 中添加检查const exists = todoList.value.some(item => item.name === name)if (exists) return alert('任务已存在')// 给 todoList 数组的末尾添加一个新对象todoList.value.push({name, id: Date.now(), finished: false})// 清空输入框title.value = ''
}
// 删除
const onDel = (index) => {if (window.confirm('确定删除么?')) {todoList.value.splice(index, 1)}
}
// 清空
const onClear = () => {if (window.confirm('确定清空所有任务嘛?')) {todoList.value = []}
}
</script>
<template><section class="todoapp"><header class="header"><h1>个人记事本</h1><!-- onAdd 监听回车事件 --><input v-model="title" placeholder="请输入任务" class="new-todo" @keyup.enter="onAdd" /><button class="add" @click="onAdd">添加任务</button></header><section class="main"><ul class="todo-list"><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>
最终效果如下: