Vue3基础(2)——组件基础

本文深入探讨Vue3的组件基础,包括组件构成、注册、样式处理及props验证。讲解了SPA的优势与缺点,Vite的使用,以及组件化开发的重要性和实践。此外,还详细阐述了组件的props、计算属性、自定义事件和v-model的应用,通过实例解析了如何在实际项目中封装和使用组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

黑马程序员Vue全套视频教程,从vue2.0到vue3.0一套全覆盖,前端必会的框架教程_哔哩哔哩_bilibili喜欢的小伙伴们别忘了投币,点赞,收藏呦,历经 5 年的反复打磨与锤炼,黑马程序员重磅推出全套最新的 Vue2 + Vue3 基础课程。共计 500 多集的免费视频,助您轻松掌握前端圈最火的 Vue 框架!https://www.bilibili.com/video/BV1zq4y1p7ga?p=327&spm_id_from=333.1007.top_right_bar_window_history.content.click

1. 单页面应用程序

单页面应用程序(SPA)是指一个Web网站中只有唯一的一个HTML页面,所有功能和交互在这个页面内完成。

1. SPA的优点:

  • 良好的交互体验:单页面内容的改变不需要重新加载整个页面;获取数据是通过Ajax异步获取;没有页面之间的跳转,不会出现白屏现象。
  • 良好的前后端工作分离模式:后端专注于提供API接口,更易实现API接口的复用;前端专注于页面的渲染,更利于前端工程化的发展。
  • 减轻服务器压力:服务器只提供数据,不负责页面的合成与逻辑的处理,吞吐能力提高几倍。

2. SPA的缺点:

  • 首屏加载慢:(解决方法)路由懒加载、代码压缩、CDN加速、网络传输压缩
  • 不利于SEO:(解决方法)SSR服务端渲染

3. vue官方提供的快速创建工程化SPA项目的方式:

  • vite(仅支持vue3.0,小而巧,速度快)
  • vue-cli(支持vue2.0和3.0,基于webpack,适用于企业项目开发)

2. Vite的基本使用

1. 创建vite项目

# 创建和初始化项目
npm init vite-app 项目名称
cd 项目名称
npm i

# 运行项目
npm run dev

2. 项目的结构

 

3. vite项目的运行流程

工程化项目中,vue通过main.js把App.vue渲染到index.html的指定区域中 。

  • App.vue用来编写带渲染的模板结构,所有模板结构需要用<template>标签包裹
  • index.html中需要预留一个el区域
  • main.js把App.vue渲染到了index.html所预留的区域中

App.vue:

<template>
  <h1>这是<i>App.vue</i>根组件</h1>
  <h3>{{username}}</h3>
  <hr>
  <p>count值是:{{count}}</p>
  <button @click="addCount">+1</button>
  <hr>
  <my-swiper></my-swiper>
  <my-test></my-test>
  <my-search></my-search>
</template>

<script>
import Search from './components/privateReg/Search.vue'

//今后,组件相关的data数据、methods方法等,都需要定义到export default导出的对象中
export default {
  // 当前组件的名称(建议:每个单词首字母大写)
  name: 'MyApp',
  // 组件的数据
  data(){
    return{
      username:'zs',
      count:0
    }
  },
  // 组件的处理函数
  methods:{
    addCount(){
      this.count++
    }
  },
  // 局部注册组件
  components:{
    'my-search':Search
  }
}
</script>

<style lang="less" scoped>
h1 {
  color: red;
  i {
    color:blue;
  }
}
</style>

main.js:

// 1、从vue中按需导入createApp函数
//createApp函数的作用:创建vue的“单页面应用程序实例”
import { createApp } from 'vue'
// 2、导入待渲染的App组件
import App from './App.vue'
import './index.css'

// 5、导入Swiper和Test两个全局注册组件
import Swiper from './components/globalReg/Swiper.vue'
import Test from './components/globalReg/Test.vue'

// 3、调用createApp()函数,返回值是“单页面应用程序的实例”
// 同时把App组件作为参数传给createApp函数,表示要把App渲染到index.html页面上
const app=createApp(App)

// 6、调用app.component()方法全局注册组件
app.component('my-swiper',Swiper)
app.component('my-test',Test)

// 4、调用“单页面应用程序的实例”的mount方法,用来指定vue实际要控制的区域
app.mount('#app')

index.html: 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

3. 组件化开发

根据封装的思想,把页面上可重用的部分封装为组件,从而提高代码复用性,方便项目的开发维护。

vue完全支持组件化开发,规定组件的后缀名为.vue。

1. vue组件构成

每个.vue 组件都由3个部分构成,分别是:

  • template:组件的模板结构
  • script:组件的Javscript行为
  • style:组件的样式
  • (其中,每个组件必须包含template模板结构,其他组成部分可选。)

(1)组件的template节点

<template>只是一个包裹性质的容器,不会真正被渲染为DOM元素。

  • <template>节点中支持使用vue指令语法,辅助开发者渲染当前组件的DOM结构。
  • 注意:vue3.x的版本中,<template>中支持定义多个根节点。

(2)组件的script节点

<script> 节点是可选的,在其中封装组件的JavaScript业务逻辑。

  • name节点:可以通过name节点为当前组件定义一个名称,在使用vue-devtools进行调试时,自定义的组件名称可以清晰的区分每个组件。
  • data节点:vue组件渲染期间需要用到的数据,可以定义在data节点中。
  • methods节点:组件中的事件处理函数必须定义在methods节点中。

(3)组件的style节点

<style> 节点是可选的,在其中编写样式美化当前组件的UI结构。

  • <style>标签上的lang="css"属性是可选的,它表示所使用的的样式语言,默认只支持普通css语法,还可以选择less、scss等。
  • 让style中支持less语法:(2)运行命令安装依赖包(2)在<style>标签加上lang="less"属性
npm install less -D

2. 组件的注册

组件之间可以相互引用,引用原则是:先注册后使用。

(1)注册组件的两种方式:

  • 全局注册组件:被全局注册的组件,可以在全局任何一个组件内使用。
    • 在main.js中,导入组件,并使用app.component()方法注册全局组件,直接以标签的形式进行使用即可。
  • 局部注册组件:被局部注册的组件,只能在当前注册的范围内使用。
    • 在需要组件的.vue模块中,导入组件,并通过components节点,为当前组件注册私有子组件,之后直接以标签的形式进行使用即可。

(2)组件注册时定义组件名称的两种方式:

  • kebab-case命名法(短横线命名法):必须严格按照短横线名称进行使用,例如my-swiper、my-search
  • PascalCase命名法(大驼峰命名法):既可以按照帕斯卡名称使用,也可以转化为短横线名称使用,例如MySwiper、MySearch
  • 注意:实际开发中推荐使用帕斯卡命名法,因为它适用性更强!

(3)通过name属性注册组件

  • 在注册组件期间,除了可以直接提供组件的注册名称之外,还可以把组件的name属性作为注册后的组件名称,相当于帕斯卡命名法。例如:app.component(Swiper.name,Swiper)

3. 组件之间的样式冲突问题

(1)默认情况下,写在.vue组件中的样式会全局生效,因此容易造成多个组件之间的样式冲突问题,根本原因是:

  • 单页面应用程序中,所有组件的DOM结构都是基于唯一的index.html页面进行呈现的。
  • 每个组件中的样式,都会影响到整个index.html页面中的DOM元素。

(2)样式冲突问题的解决方法:

  • 第一种:为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域。
  • 第二种:vue为style节点提供了scoped属性,用来自动为每个组件分配唯一的“自定义属性”,并自动为当前组件的DOM标签和style样式应用这个自定义属性,从而防止组件之间的样式冲突问题。
<template>
  <div>
    <h3 class="title">这是 List.vue 组件</h3>
    <p>这是 List.vue 中的 p 标签</p>
    <p>这是 List.vue 中的 p 标签</p>
  </div>
</template>

<script>
export default {
  name: 'MyList',
}
</script>

<style lang="less" scoped>
</style>

(3)deep样式穿透:

  • 如果给当前组件的style节点添加了scoped属性,则当前组件的样式对其子组件是不生效的。若想让某些样式对子组件生效,可以使用/deep/深度选择器。
  • 注意:/deep/是vue 2.x中实现样式穿透的方案,在vue3.x中推荐使用:deep()。
<template>
  <div>
    <h1>这是 App.vue 组件</h1>
    <p>App 中的 p 标签</p>
    <p>App 中的 p 标签</p>
    <hr />
    <my-list></my-list>
  </div>
</template>

<script>
import MyList from './List.vue'

export default {
  name: 'MyApp',
  components: {
    MyList,
  },
}
</script>

<style lang="less" scoped>
p {
  color: red;
}

:deep(.title) {
  color: blue;
}
</style>

4. 组件的props

封装vue组件的原则:

  • 组件的DOM结构、Style样式要尽量复用
  • 组件中要展示的数据,尽量由组件的使用者提供。

(1)为了方便使用者为组件提供要展示的数据,vue组件提供了props的概念。

  • props是组件的自定义属性,组件的使用者可以通过props把数据传递到子组件内部,供子组件内部进行使用。
  • props的作用:父组件通过props向子组件传递要展示的数据。
  • props的好处:提高了组件的复用性。

(2)在组件中声明props:在封装vue组件时,可以把动态地数据项声明为props自定义属性,自定义属性可以在当前组件的模板结构中被直接使用。

  • 在子组件无法使用未声明的props。
  • 可以使用v-bind属性绑定的形式,为组件动态绑定props的值。
  • 如果组件中使用驼峰命名法声明了props属性的名称,则即可以用 驼峰命名 的形式为组件绑定属性值,也可以用 短横线分割命名 的形式为组件绑定属性的值。
<my-article :title="info.title" :author="'post by ' + info.author" pub-time="1989"></my-article>
<template>
  <div>
    <h3>标题:{{title}}</h3>
    <h5>作者:{{author}}</h5>
    <h6>发布时间:{{pubTime}}</h6>
  </div>
</template>

<script>
export default {
  name: 'MyArticle',
  // 外界可以传递指定的数据,到当前的组件中
  props: ['title', 'author', 'pubTime']
}
</script>

5. 组件中进行样式绑定

(1)动态绑定HTML的class

  • 可以通过三元表达式,动态地为元素绑定class的类名。
  • 使用数组语法动态绑定class会导致模板结构臃肿的问题,此时可以使用对象语法进行简化。

(2)以对象语法绑定内联的style

  • :style的对象语法看起来像CSS,其实是一个Javascript对象。
  • CSS Property名可以用 驼峰式 或 短横线分割(记得用引号括起来)来命名。
<template>
  <div>
    <!-- <h3 class="thin" :class="isItalic ? 'italic' : ''">MyStyle 组件</h3> -->
    <!-- <h3 class="thin" :class="[isItalic ? 'italic' : '', isDelete ? 'delete' : '']">MyStyle 组件</h3>
    <button @click="isItalic = !isItalic">Toggle Italic</button>
    <button @click="isDelete = !isDelete">Toggle Delete</button> -->
    <h3 class="thin" :class="classObj">MyStyle 组件</h3>
    <button @click="classObj.italic = !classObj.italic">Toggle Italic</button>
    <button @click="classObj.delete = !classObj.delete">Toggle Delete</button>

    <hr />

    <div :style="{ color: active, fontSize: fsize + 'px', 'background-color': bgcolor }">演示文字</div>
    <button @click="fsize+=1">字号 +1</button>
    <button @click="fsize-=1">字号 -1</button>
  </div>
</template>

<script>
export default {
  name: 'MyStyle',
  data() {
    return {
      // 字体是否倾斜
      isItalic: false,
      // 是否应用删除效果
      isDelete: false,
      classObj: {
        italic: false,
        delete: false,
      },
      // 高亮时的文本颜色
      active: 'red',
      // 文字的大小
      fsize: 30,
      // 背景颜色
      bgcolor: 'pink',
    }
  },
}
</script>

<style lang="less">
// 字体变细
.thin {
  font-weight: 200;
}

// 倾斜字体
.italic {
  font-style: italic;
}

.delete {
  text-decoration: line-through;
}
</style>

4. 封装组件的案例

1. 允许用户自定义 title 标题

2. 允许用户自定义 bgcolor 背景色

3. 允许用户自定义 color 文本颜色

4. MyHeader 组件需要在页面顶部进行 fixed 固定定位,且 z-index 等于999 

/components/MyHeader.vue:

<template>
  <div class="header-container" :style="{ backgroundColor: bgcolor, color: color }">
    {{title || 'Header 组件'}}
  </div>
</template>

<script>
export default {
  name: 'MyHeader',
  props: ['title', 'bgcolor', 'color']
}
</script>

<style lang="less" scoped>
.header-container {
  height: 45px;
  background-color: pink;
  text-align: center;
  line-height: 45px;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 999;
}
</style>

App.vue:

<template>
  <div class="app-container">
    <h1>App根组件</h1>
    <my-header title="传给子组件的title" bgcolor="#000" color="#fff"></my-header>
  </div>
</template>

<script>
import MyHeader from './components/MyHeader.vue'

export default {
  name: 'MyApp',
  components: {
    MyHeader,
  },
}
</script>

<style lang="less" scoped>
.app-container {
  margin-top: 45px;
}
</style>

5. props验证

在封装组件时对外界传递过来的props数据进行合法性的校验,从而防止数据不合法的问题。

使用对象类型的props节点,可以对每个prop进行数据类型的校验。

(1)基础的类型检查:可以直接为prop属性指定基础的校验类型,从而防止组件的使用者为其绑定错误类型的数据。共有八种基础类型:String、Number、Boolean、Array、Object、Date、Function、Symbol。

(2)多个可能的类型:如果某个prop属性值的类型不唯一,此时可以通过数组的形式,为其指定多个可能的类型。

(3)必填项校验:如果数组的某个prop属性是必填项,必须让组件的使用者为其传递属性的值,此时可以通过配置对象中 required:true 的方式将其设置为必填项。

(4)属性默认值:在封装组件时,可以为通过配置对象中的 default 属性,为某个prop属性指定默认值。

(5)自定义验证函数:在封装组件是,可以为prop属性指定自定义的验证函数,从而对prop属性的值进行更加精确的控制。

<template>
  <div>
    <p>数量:{{ count }}</p>
    <p>状态:{{ state }}</p>
  </div>
</template>

<script>
export default {
  name: 'MyCount',
  // props: ['count', 'state'],
  props: {
    count: {
      type: Number,
      required: true,
      default: 100
    },
    state: Boolean,
    info: [String, Number],
    type: { // type为prop属性名
      validator(value) {
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  },
}
</script>

<style lang="less" scoped></style>

6. 计算属性

计算属性是一个function函数,实时监听data中数据的变化,并return一个计算后的新值,供组件渲染DOM时使用。

1. 声明计算属性

计算属性需要以 function 函数的形式声明到组件的 computed 选项中。渲染到页面上时直接把函数的名字丢过来就可以了。

  • 注意:计算属性侧重于得到一个计算的结果,因此计算属性中必须有return返回值!
  •            计算属性必须定义在computed节点中!
  •            计算属性必须是一个function函数!
  •            计算属性必须当做普通属性使用!

2. 计算属性 vs 方法

相对于方法来说,计算属性会缓存计算的结果,只有计算属性的依赖项发生变化时,才会重新进行匀速,因而性能更好。

<template>
  <div>
    <input type="text" v-model.number="count" />
    <p>{{ count }} 乘以 2 的值为:{{ plus }}</p>
    <p>{{ count }} 乘以 2 的值为:{{ plus }}</p>
    <p>{{ count }} 乘以 2 的值为:{{ plus }}</p>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  data() {
    return {
      count: 1,
    }
  },
  computed: {
    plus() {
      console.log('计算属性被执行了')
      return this.count * 2
    },
  },
  methods: {
    // plus() {
    //   console.log('方法被执行了')
    //   return this.count * 2
    // }
  }
}
</script>

3. 计算属性购物车实例

(1)动态计算已勾选的商品的总数量

(2)动态计算已勾选的商品的总价

(3)动态计算按钮的禁用状态

<template>
  <div class="fruit-list-container">
    <!-- 水果列表 -->
    <div class="fruit-list">
      <!-- 水果的 item 项 -->
      <div class="fruit-item" v-for="item in fruitlist" :key="item.id">
        <div class="left">
          <div class="custom-control custom-checkbox">
            <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.state" />
            <label class="custom-control-label" :for="item.id">
              <!-- 水果图片 -->
              <img :src="item.pic" alt="" class="thumb" />
            </label>
          </div>
        </div>
        <div class="right">
          <!-- 水果名称 -->
          <div class="top">{{ item.fruit }}</div>
          <div class="bottom">
            <!-- 水果单价 -->
            <span class="price">¥{{ item.price }}</span>
            <div class="btns">
              <!-- 水果数量 -->
              <button type="button" class="btn btn-light" @click="onSubClick(item.id)">-</button>
              <span class="count">{{ item.count }}</span>
              <button type="button" class="btn btn-light" @click="onAddClick(item.id)">+</button>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 结算区域 -->
    <div class="settle-box">
      <!-- TODO: 1. 动态计算已勾选的商品的总数量 -->
      <span>总数量:{{ total }}</span>
      <!-- TODO: 2. 动态计算已勾选的商品的总价 -->
      <span>总价:{{ amount }}元</span>
      <!-- TODO: 3. 动态计算按钮的禁用状态 -->
      <button type="button" class="btn btn-primary" :disabled="isDisabled">结算</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'FruitList',
  data() {
    return {
      fruitlist: [
        { id: 1, fruit: '香橼', pic: '/src/assets/images/1.jpg', price: 5, count: 1, state: true },
        { id: 2, fruit: '柚子', pic: '/src/assets/images/2.jpg', price: 4.5, count: 1, state: false },
        { id: 3, fruit: '橘子', pic: '/src/assets/images/3.jpg', price: 3, count: 1, state: false },
        { id: 4, fruit: '橙子', pic: '/src/assets/images/4.jpg', price: 6, count: 1, state: false },
        { id: 5, fruit: '粑粑柑', pic: '/src/assets/images/5.jpg', price: 6.5, count: 1, state: false },
        { id: 6, fruit: '柠檬', pic: '/src/assets/images/6.jpg', price: 4, count: 1, state: false },
        { id: 7, fruit: '青柠', pic: '/src/assets/images/7.jpg', price: 5.2, count: 1, state: false },
      ],
    }
  },
  computed: {
    // 动态计算出勾选水果的总数量
    total() {
      let t = 0
      this.fruitlist.forEach(x => {
        if (x.state) {
          t += x.count
        }
      })
      return t
    },
    // 已勾选商品的总价格
    amount() {
      let a = 0
      this.fruitlist
        .filter(x => x.state)
        .forEach(x => {
          a += x.price * x.count
        })
      return a
    },
    // 控制按钮的禁用状态
    isDisabled() {
       return this.total === 0
    }
  },
  methods: {
    // 点击了数量 -1 按钮
    onSubClick(id) {
      const findResult = this.fruitlist.find(x => x.id === id)
      if (findResult && findResult.count > 1) {
        findResult.count--
      }
    },
    // 点击了数量 +1 按钮
    onAddClick(id) {
      const findResult = this.fruitlist.find(x => x.id === id)
      if (findResult) {
        findResult.count++
      }
    },
  },
}
</script>

<style lang="less" scoped>
.fruit-list-container {
  padding-bottom: 50px;
}
.fruit-item {
  display: flex;
  padding: 10px;
  & + .fruit-item {
    border-top: 1px solid #efefef;
  }
  .left {
    margin-right: 10px;
    .thumb {
      width: 100px;
      height: 100px;
    }
  }
  .right {
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: space-between;
    .top {
      font-weight: bold;
      font-size: 13px;
    }
    .bottom {
      display: flex;
      justify-content: space-between;
      align-items: center;
      .price {
        font-size: 13px;
        font-weight: bold;
        color: red;
      }
      .btns {
        display: flex;
        align-items: center;
        .count {
          display: inline-block;
          width: 28px;
          text-align: center;
        }
      }
    }
  }
}

.custom-control-label::before,
.custom-control-label::after {
  top: 47px;
  border-radius: 10px !important;
}

.settle-box {
  height: 50px;
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  background-color: #fff;
  border-top: 1px solid #efefef;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 10px;
}
</style>

7. 自定义事件

封装组件时,为了让组件使用者可以监听到组件内状态的变化,需要用到组件的自定义事件。

1. 自定义事件的三个使用步骤

封装组件时:

  • (1)声明自定义事件
    • 开发者为自定义组件封装的自定义事件,必须事先在emits节点中声明。
  • (2)触发自定义事件
    • 在emits节点下声明的自定义事件,可以通过this.$emit('自定义事件的名称')方法进行触发。

使用组件时:

  • (3)监听自定义事件
    • 在使用自定义的组件时,通过v-on的形式监听自定义事件。

2. 自定义事件传参

在调用this.$emit()方法触发自定义事件时,可以通过第二个参数为自定义事件传参。

<template>
  <div>
    <p>count 的值是:{{ count }}</p>
    <button @click="add">+1</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  // 1. 声明自定义事件
  emits: ['countChange'],
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    add() {
      this.count++
      // 2. this.$emit() 触发自定义事件,使用第二个参数传参
      this.$emit('countChange', this.count)
    },
  },
}
</script>
<template>
  <div>
    <h1>app 根组件</h1>
    <hr />
    <my-counter @countChange="getCount"></my-counter>
  </div>
</template>

<script>
import MyCounter from './Counter.vue'

export default {
  name: 'MyApp',
  methods: {
    getCount(val) { // 形参val是子组件传过来的参数 count
      console.log('触发了 countChange 自定义事件', val)
    },
  },
  components: {
    MyCounter,
  },
}
</script>

8. 组件上的v-model

当需要维护组件内外数据的同步时,可以在组件上使用v-model指令。

在组件上使用v-model的步骤:

(1)父 --> 子同步数据:

  • ① 父组件通过v-bind:属性绑定的形式,把数据传递给子组件
  • ② 子组件中,通过props接受父组件传递过来的数据

(2)子 --> 父同步数据:

  • ① 在v-bind:指令之前添加v-model指令
  • ② 在子组件中声明emits自定义事件,格式为 update:属性值
  • ③ 通过$emit()触发自定义事件,更新父组件中的数据
<template>
  <div>
    <p>count值是:{{number}}</p>
    <button @click="add">+1</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  props: ['number'],
  emits: ['update:number'],
  methods: {
    add() {
      this.$emit('update:number', this.number + 1)
    }
  }
}
</script>
<template>
  <div>
    <h1>App 根组件  ---- {{count}}</h1>
    <button @click="count += 1">+1</button>
    <hr />
    <my-counter v-model:number="count"></my-counter>
  </div>
</template>

<script>
import MyCounter from './Counter.vue'

export default {
  name: 'MyApp',
  data() {
    return {
      count: 0,
    }
  },
  components: {
    MyCounter
  }
}
</script>

Js的数据类型: 

基本数据类型(按值访问):Undefined 、 Null 、 Boolean 、 Number 和 String

引用数据类型(按引用访问):object、Array、function


9. 任务列表案例

1. Vite创建项目

2. 组件的封装与注册:todo-input组件、todo-list组件、todo-button组件

3. props

4. 样式绑定

5. 计算属性

6. 自定义事件

7. 组件上的v-model

/components/todo-input/TodoInput.vue:

<template>
  <form class="form-inline" @submit.prevent="onFormSubmit">
    <div class="input-group mb-2 mr-sm-2">
      <div class="input-group-prepend">
        <div class="input-group-text">任务</div>
      </div>
      <input type="text" class="form-control" placeholder="请输入任务信息" style="width: 356px" v-model.trim="taskname" />
    </div>

    <button type="submit" class="btn btn-primary mb-2">添加新任务</button>
  </form>
</template>

<script>
export default {
  name: 'TodoInput',
  emits: ['add'],
  data() {
    return {
      taskname: '',
    }
  },
  methods: {
    // 表单的提交事件处理函数
    onFormSubmit() {
      if (!this.taskname) return alert('任务名称不能为空!')

      this.$emit('add', this.taskname)
      this.taskname = ''
    },
  },
}
</script>

<style lang="less" scoped></style>

/components/todo-list/TodoList.vue:

<template>
  <ul class="list-group">
    <!-- 列表组 -->
    <li class="list-group-item d-flex justify-content-between align-items-center" v-for="item in list" :key="item.id">
      <!-- 复选框 ,完成双向绑定任务状态-->
      <div class="custom-control custom-checkbox">
        <!-- App父组件通过props传递过来的list是“引用类型”的数据 -->
        <!-- 这里v-model双向绑定的结果是:用户的操作修改的是App组件中数据的状态 -->
        <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.done" />
        <!-- 动态属性绑定 -->
        <label class="custom-control-label" :class="item.done ? 'delete' : ''" :for="item.id">{{item.task}}</label>
      </div>
      <!-- 徽标 -->
      <span class="badge badge-success badge-pill" v-if="item.done">完成</span>
      <span class="badge badge-warning badge-pill" v-else>未完成</span>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TodoList',
  props: {
    list: {
      type: Array,
      required: true,
      default: [],
    },
  },
}
</script>

<style lang="less" scoped>
.list-group {
  width: 400px;
}
// 删除效果
.delete {
  text-decoration: line-through;
  color: gray;
  font-style: italic;
}
</style>

/components/todo-button/TodoButton.vue:

<template>
  <div class="mt-3 btn-container">
    <div class="btn-group" role="group" aria-label="Basic example">
      <button type="button" class="btn" :class="active === 0 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(0)">全部</button>
      <button type="button" class="btn" :class="active === 1 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(1)">已完成</button>
      <button type="button" class="btn" :class="active === 2 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(2)">未完成</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'TodoButton',
  emits: ['update:active'],
  props: {
    active: {
      type: Number,
      required: true,
      default: 0,
    },
  },
  methods: {
    onBtnClick(index) {
      if (index === this.active) return
      this.$emit('update:active', index)
    },
  },
}
</script>

<style lang="less" scoped>
.btn-container {
  width: 400px;
  text-align: center;
}
</style>

App.vue:

<template>
  <div>
    <h1>App 根组件</h1>
    <hr />
    <todo-input @add="onAddNewTask"></todo-input>
    <todo-list :list="tasklist" class="mt-2"></todo-list>
    <todo-button v-model:active="activeBtnIndex"></todo-button>
  </div>
</template>

<script>
// 导入 TodoList 组件
import TodoList from './components/todo-list/TodoList.vue'
// 导入 TodoInput 组件
import TodoInput from './components/todo-input/TodoInput.vue'
// 导入 TodoButton 组件
import TodoButton from './components/todo-button/TodoButton.vue'

export default {
  name: 'MyApp',
  data() {
    return {
      // 任务列表的数据
      todolist: [
        { id: 1, task: '周一早晨9点开会', done: false },
        { id: 2, task: '周一晚上8点聚餐', done: false },
        { id: 3, task: '准备周三上午的演讲稿', done: true },
      ],
      // 下一个可用的 Id
      nextId: 4,
      activeBtnIndex: 0,
    }
  },
  computed: {
    tasklist() {
      switch(this.activeBtnIndex) {
        case 0:
          return this.todolist
        case 1:
          return this.todolist.filter(x => x.done === true)
        case 2:
          return this.todolist.filter(x => x.done !== true)
      }
    }
  },
  methods: {
    onAddNewTask(taskname) {
      this.todolist.push({
        id: this.nextId,
        task: taskname,
        done: false,
      })

      this.nextId++
    },
  },
  components: {
    TodoList,
    TodoInput,
    TodoButton,
  },
}
</script>

<style lang="less" scoped></style>

参考文档:

ibootstrap - Bootstrap可视化布局系统LayoutIt! 可拖放排序在线编辑的Bootstrap可视化布局系统http://www.ibootstrap.cn/JavaScript indexOf() 方法https://www.w3school.com.cn/jsref/jsref_indexOf.aspJavaScript forEach() 方法 | 菜鸟教程JavaScript forEach() 方法 JavaScript Array 对象 实例 列出数组的每个元素: [mycode3 type='html'] 点我 demoP = document.getElementById('demo'); var numbers = [4, 9, 16, 25]; function myFunction(item, index) { ..https://www.runoob.com/jsref/jsref-foreach.htmlJavaScript find() 方法 | 菜鸟教程JavaScript find() 方法 JavaScript Array 对象 实例 获取数组中年龄大于 18 的第一个元素 [mycode3 type='js'] var ages = [3, 10, 18, 20]; function checkAdult(age) { return age >= 18; } function myFunction() { document.g..https://www.runoob.com/jsref/jsref-find.htmlList group · Bootstrap v4 中文文档 v4.6 | Bootstrap 中文网List groups are a flexible and powerful component for displaying a series of content. Modify and extend them to support just about any content within.https://v4.bootcss.com/docs/components/list-group/#with-badgesForms · Bootstrap v4 中文文档 v4.6 | Bootstrap 中文网Examples and usage guidelines for form control styles, layout options, and custom components for creating a wide variety of forms.https://v4.bootcss.com/docs/components/forms/#checkboxes-and-radios-1Forms · Bootstrap v4 中文文档 v4.6 | Bootstrap 中文网Examples and usage guidelines for form control styles, layout options, and custom components for creating a wide variety of forms.https://v4.bootcss.com/docs/components/forms/#inline-formsButton group · Bootstrap v4 中文文档 v4.6 | Bootstrap 中文网Group a series of buttons together on a single line with the button group, and super-power them with JavaScript.https://v4.bootcss.com/docs/components/button-group/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值