Ⅰ. 脚手架
一、Vue开发方式
1. 传统开发模式
举个例子:

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
Vue.createApp({
setup() {
const msg = 'Hello World'
return { msg }
}
}).mount('#app')
</script>判断是否为传统写法的依据:
- 是否使用
new Vue()
new Vue({...})是 Vue 2 的标准写法,也是 "全局 Vue 对象 + 根实例" 的模式。现代 Vue 3 项目里,一般不会直接用
new Vue(),而是用createApp(App)来创建应用实例。- 是否在 CDN 环境下
- 如果你直接在浏览器里
<script src="``https://cdn.jsdelivr.net/.../vue.js``"></script>然后写new Vue({...}),通常就是快速 demo / 教学 / 小型页面 ,不使用模块化打包工具。- 语法风格
使用
data+methods+Vue.set这种选项式 API,也属于传统开发模式。现代 Vue 3 推荐使用 组合式 API +
<script setup>,不需要Vue.set,逻辑可拆分为可复用的 composable 函数。
-
优点:简单、上手快
-
缺点:功能单一、开发体验差
2. 工程化开发模式
import { createApp, ref } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')在构建工具(Vite/Webpack)环境下开发 Vue,这是最推荐的、也是企业采用的方式。其中 App.vue 是一个单文件组件(SFC),会被构建工具编译成 JS 模块。

-
优点:
-
功能全面
-
开发体验好
-
...
-
-
缺点:
-
目录结构复杂
-
理解难度提升
-
| 特点 | 教学版(CDN) | 模块化(ES Module) |
|---|---|---|
| 适用场景 | 快速演示、小项目 | 现代大型项目 |
| 引入方式 | <script src="vue.js"> |
import { createApp } from 'vue' |
| 组件写法 | 直接写在 createApp({ setup(){...} }) |
单文件组件 .vue,逻辑写在 setup() |
| 推荐度 | 初学者 | 实战项目 |
二、准备工程化环境
1. 安装 Nodejs
node -v
npm -vnpm换源:
// 查看 npm 源
npm config get registry
// 默认是指向 https://registry.npmjs.org/,也就是官方源
// 为了提高npm下载速度, 可以给npm换源
// 国内源有很多,我这里用淘宝源吧。毕竟是大公司,会比较稳定
npm config set registry https://registry.npmmirror.com
// 再一次查看 npm 源
npm config get registry2. 安装 yarn 或 pnpm
yarn 和 pnpm、还有 npm 三者的功能类似,都是包管理工具,用来下载或删除模块包,性能上 yarn 和 pnpm 优于 npm。
# windows系统
npm install yarn -g
npm install pnpm -g
___________________________________
# mac系统
sudo npm install yarn -g
sudo npm install pnpm -g检测是否安装成功:
yarn -v
pnpm -v三、创建Vue工程化项目
-
选定一个存放位置,比如选择桌面,根据自己情况,选择D盘或E盘等
-
执行命令
npm create vue@latest,会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具 -
进入项目根目录:
cd 项目名称 -
安装 vue 等模块依赖:
npm i -
启动项目:
npm run dev,会开启一个本地服务器 -
浏览器网址栏输入:http://localhost:5173
四、认识脚手架目录及文件

五、分析3个入口文件的关系

-
main.js:整个项目打包的入口,用于创建应用,是 Vue 代码通向网页代码的桥梁 -
App.vue:Vue 代码的入口(根组件) -
index.html:项目入口网页
六、Vue单文件
-
vue 推荐采用
.vue的文件来开发项目 -
一个 vue 文件通常有3部分组成,
script``(JS)+template``(HTML)+style``(CSS) -
一个 vue 文件是独立的模块,作用域互不影响
-
style 部分通常配合
scoped属性,保证样式只针对当前template内的标签生效 -
vue 文件会被
vite打包成 js、css 等,最终交给 index.html 通过浏览器呈现效果
七、setup简写 + 插值表达式 + 响应式
1. 传统写法
<script>
export default {
setup() {
// ...
const msg = 'Hello Vue3+Vite'
return {
msg
}
}
}
</script>
<template>
<h1>{{ msg }}</h1>
</template>
<style></style>2. 现代写法(推荐)
<script setup>
const msg = 'Hello Vue3+Vite'
</script>
<template>
<h1>{{ msg }}</h1>
</template>
<style></style>3. 代码示例
<script setup>
// 导入响应式函数
import { reactive, ref } from 'vue'
// 字符串
const msg = ref('Hello Vue3+Vite')
// 对象
const obj = reactive({
name: '小vue',
age: 9
})
// 函数
function fn() {
return 100
}
</script>
<template>
<!-- 1. 直接放变量 -->
<h1>{{ msg }}</h1>
<!-- 2. 对象.属性 -->
<p>我叫 {{ obj.name }}, 今年 {{ obj.age }} 岁</p>
<!-- 3. 三元表达式 -->
<p>{{ obj.age >= 18 ? '已成年' : '未成年' }}</p>
<!-- 3. 算数运算 -->
<p>来年我就 {{ obj.age + 1 }} 岁了</p>
<!-- 4. 函数的调用 -->
<p>fn的返回值是: {{ fn() }}</p>
<!-- 4. 方法的调用 -->
<p>{{ msg }} 中含有 {{ msg.split(' ').length }} 个单词</p>
</template>Ⅱ. 指令
一、Vue中的常用指令
指令是 Vue 提供的带有 v- 前缀的特殊标签属性,用来增强标签、提高标签数据渲染的能力。vue3 中的指令按照不同的用途可以分为如下六大类:
-
内容渲染指令(v-html、v-text)
-
属性绑定指令(v-bind)
-
事件绑定指令(v-on)
-
条件渲染指令(v-show、v-if、v-else、v-else-if)
-
列表渲染指令(v-for)
-
双向绑定指令(v-model)
二、内容渲染指令:v-html && v-text
作用:辅助开发者渲染 DOM 元素的文本内容。
-
v-text(类似innerText)- 类似 innerText,使用该语法,只会覆盖 p 标签原有内容,不会解析标签。
-
v-html(类似innerHTML)- 类似 innerHTML,使用该语法,不仅会覆盖 p 标签原有内容,还能够将 HTML 标签的样式呈现出来。
代码示例:
<script setup>
import { ref } from 'vue'
const str = ref('<span style="color:red;">Hello Vue3+Vite</span>')
</script>
<template>
<div>
<p v-text="str"></p>
<p v-html="str"></p>
</div>
</template>
<style scoped></style>
三、属性绑定指令:v-bind
作用:把表达式的结果与标签的属性动态绑定。
语法如下所示:
-
v-bind:属性名="表达式" -
可简写成:
:属性名="表达式"代码实例:
<script setup>
import { ref } from 'vue'
const url = ref('http://www.baidu.com')
</script>
<template>
<div>
<!-- v-bind: 标签属性动态绑定 -->
<a **v-bind:href="url"** >百度一下</a>
<!-- 简写 -->
<a **:href="url"** >百度一下</a>
</div>
</template>四、事件绑定指令:v-on
作用:给元素绑定事件。
有三种不同语法,以给按钮添加点击事件为例:
<button v-on:事件名="内联语句">按钮</button>
<button v-on:事件名="处理函数">按钮</button>
<button v-on:事件名="处理函数(实参列表)">按钮</button>-
v-on:可以简写为@ -
处理函数需要在
<script>下声明
代码示例:
<script setup>
import { ref } from 'vue'
// 响应式数据 计数器
const count = ref(0)
// 无参函数
const increase = () => {
count.value++
}
// 有参函数
const add = (n) => {
count.value +=n
}
</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(3)">+3</button>
<button v-on:click="add(5)">+5</button>
<!-- 4. 简写 @click -->
<button @click="add(5)">+5</button>
</div>
</template>五、条件渲染指令:v-show && v-if
作用:辅助开发者控制 DOM 的显示与隐藏。
-
v-show-
作用:控制元素显示隐藏
-
语法:
v-show="布尔表达式",表达式值为 true 显示,false 隐藏 -
原理: 切换
display:none控制显示隐藏 -
场景:适合需要频繁切换显示隐藏的场景
-
-
v-if-
作用:控制元素显示隐藏(条件渲染)
-
语法:
v-if="布尔表达式",表达式值 true 显示,false 隐藏 -
原理: 基于条件判断,创建或移除 DOM 元素
-
场景:适合不需要频繁切换的场景
-
-
v-else和v-else-if-
作用:辅助 v-if 进行判断渲染
-
语法:
v-else v-else-if="表达式" -
需要紧接着 v-if 使用
-
代码示例:
<script setup>
import { ref } from 'vue'
// 是否可见
const visible = ref(true)
// 是否登录
const isLogin = ref(true)
// 成绩
const mark = ref(100)
</script>
<template>
<!-- v-show -->
<div class="red" **v-show="visible"** ></div>
<!-- v-if -->
<div class="green" **v-if="visible"** ></div>
<hr>
<!-- 双分支的条件渲染 -->
<div **v-if="isLogin"** >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: red;
}
.green {
background: green;
}
</style>案例:学习之旅
默认展示数组中的第一张图片,点击上一页下一页来回切换数组中的图片

-
数组存储图片路径
['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'
]
// 当前下标, 默认值 0
const i = ref(0)
</script>
<template>
<div>
<button
v-if="i >= 1"
@click="i--">
上一页
</button>
</div>
<img
:src="imgList[i]"
alt="img" />
<div>
<button
v-if="i < imgList.length - 1"
@click="i++">
下一页
</button>
</div>
</template>
<style>
#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>案例:可折叠面板

<script setup>
import { ref } from 'vue'
// 是否可见, 默认值 false, 表示不可见
const visible = ref(false)
</script>
<template>
<h3>可折叠面板</h3>
<div class="panel">
<div class="title">
<h4>自由与爱情</h4>
<span
class="btn"
@click="visible = !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;
border: 1px solid #ccc;
padding: 0 1em;
}
.title h4 {
line-height: 2;
margin: 0;
}
.container {
border: 1px solid #ccc;
padding: 0 1em;
}
.btn {
/* 鼠标改成手的形状 */
cursor: pointer;
}
}
</style>六、列表渲染指令:v-for
作用:基于数组进行遍历列表渲染
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: 299 },
{ id: 2, name: '足球', price: 99 },
{ id: 3, name: '排球', price: 199 }
])
// 准备对象
const obj = {
id: 10001,
name: 'liren',
age: 9
}
</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 scoped></style>案例:书架
-
根据左侧数据渲染出右侧列表(
v-for) -
点击删除按钮时,应该把当前行从列表中删除(获取当前行的
index,利用splice删除)

<!-- @format -->
<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: '施耐庵' }
])
// 删除函数
const onDel = (i) => {
// i: 当前点击的下标
if (window.confirm('确定删除么?')) {
// 调用 splice 进行删除
bookList.value.splice(i, 1)
}
}
</script>
<template>
<h3>书架</h3>
<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>为什么要给 v-for 添加 key
语法::key="唯一值"
作用:给列表项添加的唯一标识,便于 Vue 进行列表项的正确排序复用,因为 Vue 的默认行为会尝试原地修改元素(就地复用)
注意事项:
-
key的类型只能是数字或字符串 -
key的值必须唯一, 不能重复 -
推荐用
id作为key(因为id唯一),不推荐用index作为key(会变化)
七、双向绑定指令:v-model
所谓双向绑定就是:
-
数据变了->视图的变化
-
视图变了->数据的变化
作用在表单元素(input、select、radio、checkbox)上,实现数据双向绑定,从而可以快速获取或设置表单元素的值。
语法:v-model="响应式数据"
使用双向绑定实现以下需求:
-
点击登录按钮获取表单中的内容
-
点击重置按钮清空表单中的内容

<script setup>
import { reactive } from 'vue'
// 登录表单对象
const loginForm = reactive({
username: '',
password: ''
})
</script>
<template>
<div class="login-box">
账户:<input type="text" **v-model="loginForm.username"** /><br /><br />
密码:<input type="password" **v-model="loginForm.password"** /><br /><br />
<button>登 录</button>
<button>重 置</button>
</div>
</template>案例:记事本
新建 styles/index.css 文件:
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%2
2%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%20
fill%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%2
2%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%20
fill%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%2
027%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;
}
}App.vue文件:
<script setup>
// 导入 todo 样式
import './styles/index.css'
// 代办任务列表
const todoList = [
{ id: 321, name: '吃饭', finished: false },
{ id: 666, name: '睡觉', finished: true },
{ id: 195, name: '打豆豆', finished: false }
]
</script>
<template>
<section clsss="todoapp">
<header class="header">
<h1>记事本</h1>
<input
placeholder="请输入任务"
class="new-todo" />
<button class="add">添加任务</button>
</header>
<section class="main">
<ul class="todo-list">
<li class="todo">
<div class="view">
<span class="index">1.</span> <label>吃饭饭</label>
<button class="destroy"></button>
</div>
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">合 计: <strong> 0 </strong></span>
<button class="clear-completed">清空任务</button>
</footer>
</section>
</template>功能需求
-
列表渲染
<script setup> import { ref } from 'vue' // 代办任务列表 const todoList = ref([ { id: 321, name: '吃饭饭', finished: false }, { id: 666, name: '睡觉觉', finished: true }, { id: 195, name: '打豆豆', finished: false } ]) </script> <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"></button> </div> </li> </ul> -
添加功能
<script setup> // 任务名称 const title = ref('') // 添加函数 const onAdd = () => { // 去除首尾空格 const name = title.value.trim() // 非空校验 if (!name) return alert('名称不能为空') // 添加至数组 todoList.value.push({ name, id: Date.now(),// 时间戳 finished: false }) // 清空输入框 title.value = '' } </script> <header class="header"> <h1>记事本</h1> <input **v-model="title"** placeholder="请输入任务" class="new-todo" **@keydown.enter="onAdd"** /> <button class="add" **@click="onAdd"** > 添加任务 </button> </header> -
删除功能
<script setup> // 删除函数 const onDel = (index) => { if (window.confirm('确认删除么?')) { todoList.value.splice(index, 1) } } </script> <button class="destroy" **@click="onDel(index)"** > </button> -
底部统计和清空
<script setup> // 清空函数 const onClear = () => { todoList.value = [] } </script> <footer class="footer"> <span class="todo-count"> 合 计: <strong> {{ todoList.length }} </strong> </span> <button class="clear-completed" **@click="onClear"** > 清空任务 </button> </footer>