脚手架:一个保证各项工作顺利开展的平台,方便我们 拿来就用,零配置
相比直接 script 引入 vue 源码,有没有更好的方式编写vue代码呢?
① 传统开发模式:
<script src="vue.js"></script>
② 工厂化开发模式:
在 构建工具(Vite/Webpack )环境下开发Vue,这是最推荐的、也是企业采用的方式
① 安装工具 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
创建步骤如下:
npm create vue@latest
,会安装并执行 create-vue
, 它是Vue官方的项目脚手架工具cd
项目名称npm i
npm run dev
,会开启⼀个本地服务器,然后在浏览器网址栏输入:http://localhost:5173C:\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-vue
T 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
> vite
VITE 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)
用 vscode 打开查看,如下:
我们今后Vue代码写哪个目录下?
vite
打包成 css/js/img
, 然后交给 index.html
,最终通过浏览器呈现在用户眼前分析上面三个入口文件关系:
1、main.js、App.vue、index.html三个文件的作用?
2、mian.js、App.vue、index.html 三者的关系是什么?
思考:代码写一起,会不会出现class类名、js变量名 重名冲突?Vue中如何避免呢?
vue单文件介绍
.vue
的文件来开发项目script(JS)+template(HTML)+ style(CSS)
scoped
属性,保证样式只针对当前 template 内的标签生效作用:提供了独立的作用域,不用担心 JS 变量名、CSS 选择器名冲突
注意:.vue 文件浏览器无法识别,需要借助 vite打包成 js、css 等,最终交给 index.html
,通过浏览器呈现效果
补充内容
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')
实例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>
指令(Directives)是Vue提供的带有v-前缀的特殊标签属性,用来增强标签的能能力
vue3 中的指令按照不同的用途可以分为如下 6 大类:
内容渲染指令用来辅助开发者渲染 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
语法:
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>
使用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>
v-show
display:none
控制显示隐藏v-if
v-if="布尔表达式"
【表达式值 true显示,false 隐藏】v-else 和 v-else-if
v-else
v-else-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>
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>
语法 :key="唯一值"
代码如下:
<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>
删除时结果如下:
注意:
所谓双向绑定就是:
作用:在 表单元素(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>
效果如下:
实现思路
代码如下:
<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>
效果如下:
实现思路
代码如下:
<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” 之后,项目运行失败
npm i -D sass
效果如下:
需求:
代码如下:
<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>
在 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=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;
}
}
代码如下:
<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>
最终效果如下:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有