webpack+vue从零开始打造todoList2

本章节开始todoList项目实现。

项目开始前的配置,包括入口,出口,打包,创建实例挂载等,前文已经介绍完成。这里不重复了,也可以直接安装脚手架开始项目功能。

首先展示出源文件的目录结构:

|-- src,
      |-- App.vue,
      |-- main.js,
      |-- assets,
      |   |-- images,
      |   |   |-- bg.jpg,
      |   |   |-- Checked.svg,
      |   |   |-- unChecked.svg,
      |   |-- styles,
      |       |-- global.styl,
      |       |-- reset.css,
      |       |-- test.css,
      |-- components,
          |-- MainFooter.vue,
          |-- MainHeader.vue,
          |-- MainTodo,
              |-- MainTodo.vue,
              |-- _coms,
                  |-- TodoInfo.vue,
                  |-- TodoItem.vue,

一 。项目拆分

  • MainHeader 组件:页头
  • MainTodo 组件:功能
    • TodoItem 组件:输入内容后添加到列表上
    • TodoInfo 组件:按钮功能(已完成查看,未完成查看,清除)
  • MainFooter 组件:页脚

二。MainHeader

MainHeader.vue

<template>
    <header class="main-header">
        <h1>TodoList</h1>
    </header>
</template>
<script>
export default {
    name:'MainHeader'
}
</script>
<style lang="stylus" scoped>
.main-header
    text-align :center
    h1
        margin :20px
        font-size: 100px
        font-weight: 300
        color: rgb(252,157,154)
        text-shadow: 5px 5px 5px rgba(0,0,0,0.3)
        
</style>

App.vue

<template>
    <div>
        <main-header></main-header>
    </div>
</template>

<script>
import './assets/styles/global.styl'
import MainHeader from './components/MainHeader.vue'
export default {
    name:'App',
    components: {
        //组件名:组件对象
        MainHeader: MainHeader,
    }
}
</script>

<style lang="stylus" scoped>

</style>

优化:一些会反复用到的样式,建议提取出来做个文件

在 styles 目录下,创建 theme.styl, 将主题配色定义成变量

$red = rgb(254, 67, 101)
$lightred = rgb(252, 157, 154)
$yellow = rgb(249, 205, 173)
$green = rgb(131, 175, 155)
$lightgreen = rgb(200, 200, 169)

1.在需要用到的时候, 引入相关的文件

修改 MainHeader.vue

<style lang="stylus" scoped>
@import '../assets/styles/theme.styl'

...
  h1
    color: $lightred

</style>

2.加入别名

在 webpack 的配置中, 可以指定别名

resolve: {
  alias: {
    'vue': 'vue/dist/vue.js',
    '@': path.resolve(__dirname, '../src'),
    'styles': path.resolve(__dirname, '../src/assets/styles'),
'images': path.resolve(__dirname, '../src/assets/images'),
} },

stylus引入修改 :@import '~styles/theme.styl'

~ 被看做模块间的依赖

三。MainTodo

  • MainTodo -- input输入框
    • TodoItem 子组件 列表
    • TodoInfo  子组件 选择按钮

App.vue组件挂载,参考上面。

1. MainTodo.vue

<template>
  <div class="main-todo">
    <input type="text" class="add-todo" placeholder="what to do?" autofocus />
  </div>
</template>
<script>
export default {
    name:'MainTodo'//组件命名尽量和文件名统一
}
</script>
<style lang="stylus" scoped>
.main-todo
  margin: 0 auto
  width: 600px
  background-color: #fff
  box-shadow: 0 0 5px #666

   .add-todo
    padding: 16px 16px 16px 36px
      width: 100%
      font-size: 24px
      font-family: inherit
      font-weight: inherit //继承
      color: inherit
      border: none
      outline: none
      box-sizing: border-box //盒子模型
</style>

 2. TodoItem页面实现

子组件挂载到父组件MainTodo.vue 上

<template>
  <div class="main-todo">
    <input type="text" class="add-todo" placeholder="what to do?" autofocus />
    <todo-item></todo-item>
  </div>
</template>
<script>
import TodoItem from './_coms/TodoItem.vue' 
export default {
    name:'MainTodo',
    components: {
        TodoItem
    }
}
</script>
...

TodoItem.vue 页面样式

<template>
  <div class="todo-item">
    <input type="checkbox" />
    <label>todo1</label>
    <button></button>
  </div>
</template>
<script>
export default{
    name:'TodoItem'
}
</script>
<style lang="stylus" scoped>
@import '~styles/theme.styl'
.todo-item
  padding : 10px
  display :flex //弹性盒子
  justify-content :space-between //两端对齐
  align-items :center //垂直居中
  font-size 24px
  &:hover
    button:after 
      content 'x'
      font-size 24px
      color $lightred
      cursor pointer
  &.completed
    label
      color #d9d9d9
      text-decoration line-through
  label
    flex:1  //该元素占据剩下全部空间
    transition :color .4s //过渡效果
  input
    width 50px
    height 30px
    text-align center
    border none
    outline none
    appearance none //清除input默认样式
    &:after    //&:父元素选择器 
      content url('~images/unchecked1.svg')
    &:checked:after
      content url('~images/check1.svg')
  button
    width 40px
    background-color transparent
    border none
    outline none
    appearance none

</style>

---- 包含三个元素:选择框,label,button删除按钮

---- 实现基本样式:元素之间两端对齐,垂直居中;选择框、button 按钮清除原有样式;input设置显示矢量图,因层级较多,调用引入别名;button显示文本‘ x ’表示删除。

优化:在 styles 文件夹下创建 mixins.styl, 将一些通用样式封装成函数 (mixins)。

mixins.styl

cleanDefaultStyle()
  appearance: none
  border: none
  outline: none

'{ }' , ' ; ' 这些标点我都省略了,这是因为我的 vs code插件stylus配置过了,打包生成时都会自动添加。

使用:

<style lang="stylus" scoped>
@import '~styles/theme.styl'
@import '~styles/mixins.styl'
...
  input
    width: 50px
    height: 30px
    text-align: center
    cleanDefaultStyle()
...
</style>

3 TodoItem 业务实现

需求分析:

  • 添加功能
  • 选中功能
  • 删除功能

1)添加功能

  1. 输入内容, 按下键盘回车键时, 添加一条待办记录
  2. 如果没有输入内容, 不添加
  3. 添加后, 之前的内容清空

核心点:父组件要向子组件进行传值

MainTodo.vue

<template>
  <div class="main-todo">
    <input type="text" class="add-todo" placeholder="what to do?" autofocus v-model="content" @keyup.enter="addTodo"/>
    <todo-item v-for="(item,index) in todoData" :key="index" :todo="item"></todo-item>
  </div>
</template>
<script>
import TodoItem from './_coms/TodoItem.vue'
let i= 0
export default {
    name:'MainTodo',
    components: {
        TodoItem
    },
    data(){
      return{
        todoData:[
          // {
          //   id:0,
          //   content: 'todo1',
          //   completed: false
          // },
          // {
          //   id: 1,
          //   content: 'todo2',
          //   completed: false
          // }添加功能完成,清空测试数据
        ],
        content:''
      }
    },
    methods:{
      addTodo(){
        if(this.content === '') return

        this.todoData.unshift({
          id:i++,  //id: 2
          content:this.content,
          completed:false
        })

        this.content = ''
      }
    }
}
</script>
<style lang="stylus" scoped>
.main-todo
  margin: 0 auto
  width: 600px
  background-color: #fff
  box-shadow: 0 0 5px #666

  .add-todo
    padding: 16px 16px 16px 36px
    width: 100%
    font-size: 24px
    font-family: inherit
    font-weight: inherit
    color: inherit
    border: none
    outline: none
    box-sizing: border-box
</style>

=>

定义一个todoData数据对象,添加测试数据,在todo-item组件上循环遍历,这时候页面展示的数据还是"todo1"测试数据,要绑定一个:todo="item"获取数据对象传递给子组件。

定义数据content,用v-model使content和input文本双向绑定,添加一个键盘enter事件操作:

当content为空,返回return;否则向数据对象中添加数据unshift,清空content 。

=>

TodoItem修改显示从父组件接收过来的数据

<template>
  <div class="todo-item">
    <input type="checkbox" />
    <label>{{todo.content}}</label>
    <button></button>
  </div>
</template>
<script>
export default{
    name:'TodoItem',
    props:{
      todo: Object //获取父组件传递的object类型的todo数据
    }
}
</script>

2)选中功能

  1. 点击选中按钮, 按钮变为选中样式, 并且文本显示被删除(有删除线)
  2. 再次点击选中按钮, 按钮变为末选中样式, 并且文本正常显示(没有删除线)

核心点:样式绑定

TodoItem.vue

<template>
  <!-- <div class="todo-item"> -->
  <div :class="['todo-item',todo.completed ? 'completed': '']">
    <input type="checkbox" v-model="todo.completed" />
    <label>{{todo.content}}</label>
    <button></button>
  </div>
</template>
  • 前面我们已经写好了input的选中样式和label的删除样式(类completed)。
  • 给div添加多种样式绑定,通过父组件传递过来的completed的值判断是否添加类。
  • todo.completed默认值是false,可以和input数据绑定,当input选中后,.completed的值即为true。

3)删除功能

  1. 点击删除按钮时, 删除这条待办记录

核心点:子组件向父组件传值

TodoItem.vue

<template>
  <div :class="['todo-item',todo.completed ? 'completed': '']">
    <input type="checkbox" v-model="todo.completed" />
    <label>{{todo.content}}</label>
    <button @click="delItem"></button>
  </div>
</template>
<script>
export default{
    name:'TodoItem',
    props:{
      todo: Object
    },
    methods:{
      delItem(){
        this.$emit('del',this.todo.id)
      }
    }
}
</script>
...

=> 给button按钮添加一个点击事件delItem,在点击事件中我们通过$emit去监听父组件的事件del,并传递参数todo.id.

MainTodo.vue

<template>
  <div class="main-todo">
    <input type="text" class="add-todo" placeholder="what to do?" autofocus v-model="content" @keyup.enter="addTodo"/>
    <todo-item v-for="(item,index) in todoData" :key="index" :todo="item" @del="handleDeleteItem"></todo-item>
  </div>
</template>
<script>
import TodoItem from './_coms/TodoItem.vue'
let i= 0
export default {
    name:'MainTodo',
    components: {
        TodoItem
    },
    data(){
      return{
        todoData:[],
        content:''
      }
    },
    methods:{
      addTodo(){
        if(this.content === '') return

        this.todoData.unshift({
          id:i++,  //id: 2
          content:this.content,
          completed:false
        })

        this.content = ''
      },
      handleDeleteItem(id){
        this.todoData.splice(this.todoData.findIndex(item=> item.id === id),1)
      }
    }
}
</script>
...

=> 给组件todo-item 添加 del 事件,使用handleDeleteItem方法并接收参数id,在del事件中使用findIndex遍历数据获取到当前id对应的索引值,使用splice删除该条数据。

4.TodoInfo页面实现

  • 总计
  • tab 选项卡
  • 清除框

在 MainTodo.vue 中引入 TodoInfo 组件,参考上面TodoItem。

TodoInfo.vue

<template>
  <div class="todo-info">
    <span class="total">1 item left</span>
    <div class="tabs">
      <a v-for="(item, index) in states" :key="index">{{ item }}</a>
    </div>
    <button class="clear">Clear Completed</button>
  </div>
</template>

<script>
export default {
  name: 'TodoInfo',
  data() {
    return {
      states: ['all', 'active', 'completed']
    }
  },
  
}
</script>

<style lang="stylus" scoped>
@import '~styles/theme.styl'
.todo-info
    display flex
    justify-content space-between
    font-weight 400
    padding 5px 10px
    line-height 30px
    border-top 1px solid rgba(0,0,0,0.1)
    .total
      color $red
    .tabs
      display flex
      justify-content space-between
      width 200px
      a
        border 1px solid $lightred
        padding 0 10px 
        border-radius 5px
        &.actived
          background-color $lightred
          color #fff
    .clear
      padding 0 10px
      background-color $green
      border-radius 5px
      color #fff
      appearance none
      border none
      outline none
</style>

优化代码,公用样式提取封装,语义化标签

mixins.styl

cleanDefaultStyle(){
  appearance: none
  border: none
  outline: none
}

btn(c, border = false){
  padding: 0 10px
  border-radius: 5px
  cursor: pointer
  cleanDefaultStyle()

  if (border == true)
    border: 1px solid c
  else
    background-color: c
    color: #fff
}

primaryBtn()
  btn(rgb(252, 157, 154))

primaryBorderBtn()
  btn(rgb(252, 157, 154), true)

infoBtn()
  btn(rgb(131, 175, 155))

TodoInfo.vue

<template>
  <div class="todo-info">
    <span class="total">1 item left</span>
    <div class="tabs">
      <a class="btn primary border" v-for="(item, index) in states" :key="index">{{ item }}</a>
    </div>
    <button class="btn info">Clear Completed</button>
  </div>
</template>

<script>
export default {
  name: 'TodoInfo',
  data() {
    return {
      states: ['all', 'active', 'completed']
    }
  },
  
}
</script>

<style lang="stylus" scoped>
@import '~styles/theme.styl'
@import '~styles/mixins.styl'
.todo-info
    display flex
    justify-content space-between
    font-weight 400
    padding 5px 10px
    line-height 30px
    border-top 1px solid rgba(0,0,0,0.1)
    .total
      color $red
    .tabs
      display flex
      justify-content space-between
      width 200px
    .btn.primary.border
      primaryBorderBtn()
      &.actived
       primaryBtn()
    .btn.info
      infoBtn()
</style>

5.TodoInfo业务实现

  • 统计功能
  • 切换显示不同状态功能
  • 删除功能

1)统计功能

  1. 实时统计剩余的待办事项

核心点:监听器watch

 MainTodo.vue

<template>
  <div class="main-todo">
    ...
    <todo-info :total="total"></todo-info>
  </div>
</template>
<script>
...
    data(){
      return{
        todoData:[],
        content:'',
        total:0
      }
    },
    methods:{
...
    },
    watch:{
      todoData:{
        deep:true,
        handler(){
          this.total= this.todoData.filter(item=> item.completed == false).length
        }
      }
    }
}
</script>
...
  • deep: true -- 表示监听每个一个属性的变化
  • handler-- 处理函数, 在此是为了过滤没有完成的数组, 并获取长度

通过监听todoData数据,在函数中获取到属性为completed= false 的数组的总数,绑定数据 :total='total',把数据传递给子组件。

注意:前面的total是子组件要使用的变量,引号内的是父组件要传递的变量

TodoInfo.vue

...
    <span class="total">{{total}} item left</span>
...
<script>
...
  props:{
    total: Number
  },
 ...
</script>

props接收数据,类型是number,显示页面上

2) 切换功能

  1. 点击不同的状态分别显示不同状态的待办事项
  2. 点击 all, 显示所有的待办事项
  3. 点击 active, 显示未完成的待办事项
  4. 点击 completed, 显示已完成的待办事项

核心点:计算属性computed

TodoInfo.vue

<template>
  <div class="todo-info">
    <span class="total">{{total}} item left</span>
    <div class="tabs">
      <a :class="['btn','primary','border', state == item ? 'actived' : '']" v-for="(item, index) in states" :key="index" @click="toggleState(item)">{{ item }}</a>
    </div>
    <button class="btn info">Clear Completed</button>
  </div>
</template>

<script>
export default {
  name: 'TodoInfo',
  props:{
    total: Number
  },
  data() {
    return {
      states: ['all', 'active', 'completed'],
      state:'all'
    }
  },
  methods:{
    toggleState(item){
      this.state = item,
      this.$emit('toggleState',this.state)
    }
  }
  
}
</script>

选中样式绑定,这里就不在重复了。

添加一个点击事件,通过$emit监听父组件的事件toggleState,并传递当前的item值

MainTodo.vue

<template>
  <div class="main-todo">
    <input type="text" class="add-todo" placeholder="what to do?" autofocus v-model="content" @keyup.enter="addTodo"/>
    <todo-item v-for="(item,index) in filterData" :key="index" :todo="item" @del="handleDeleteItem"></todo-item>
    <todo-info :total="total" @toggleState="handleToggleState"></todo-info>
  </div>
</template>
<script>
import TodoInfo from './_coms/TodoInfo.vue'
import TodoItem from './_coms/TodoItem.vue'
let i= 0
export default {
    name:'MainTodo',//组件命名尽量和文件名统一
    components: {
        TodoItem,
        TodoInfo,
        TodoInfo,
    },
    data(){
      return{
        todoData:[],
        content:'',
        total:0,
        filter:'all'
      }
    },
    methods:{
      addTodo(){
        if(this.content === '') return

        this.todoData.unshift({
          id:i++,  //id: 2
          content:this.content,
          completed:false
        })

        this.content = ''
      },
      handleDeleteItem(id){
        this.todoData.splice(this.todoData.findIndex(item=> item.id === id),1)
      },
      handleToggleState(state){
        this.filter = state
      }
    },
    computed:{
      filterData(){
          switch (this.filter){
          case 'all': 
            return this.todoData
            break
          case 'active':
            return this.todoData.filter(item => item.completed == false)
            break
          case 'completed':
            return this.todoData.filter(item => item.completed == true)
            break
          }
      }
    },
    watch:{
      todoData:{
        deep:true,
        handler(){
          this.total= this.todoData.filter(item=> item.completed == false).length
        }
      }
    }
}
</script>

在父组件定义事件toggleState,绑定handleToggleState方法获取到传递过来的参数。在计算属性中定义filterData方法返回todoDate筛选后的数据,并通过v-for来遍历它,达到选中切换效果。

3)删除功能

需求:点击clear删除所有已完成的事项

=>参考上面选中方法:定义一个点击事件,通过$emit监听执行父组件的删除事件,不用传值,直接筛选出未完成的数组赋值给todoData

TodoInfo.vue

<button class="btn info" @click='clearCompleted'>Clear Completed</button>
methods:{
    clearCompleted(){
      this.$emit('clearCompleted')
    }
}

MainTodo.vue

<todo-info :total="total" @toggleState="handleToggleState" @clearCompleted="handleClear"></todo-info>
 handleClear(){
        this.todoData = this.todoData.filter(item => item.completed == false)
      }

 四。MainFooter

版权所有,是纯文本内容,不需要业务代码,编写好组件内容,挂载到vue即可。

MainFooter.vue

<template>
  <footer class="main-footer">Written By Name</footer>
</template>

<style lang="stylus" scoped> 
.main-footer
  margin: 20px auto
  text-align: center
  color: #fff
  text-shadow: 5px 5px 5px #000
</style>

App.vue

<template>
    <div>
        <main-header></main-header>
        <main-todo></main-todo>
        <main-footer></main-footer>
    </div>
</template>

<script>
import './assets/styles/global.styl'
import MainHeader from './components/MainHeader.vue'
import MainTodo from './components/MainTodo/MainTodo.vue'
import MainFooter from './components/MainFooter.vue'
export default {
    name:'App',
    components: {
        //组件名:组件对象
        MainHeader: MainHeader,
        MainTodo,
        MainFooter
    }
}
</script>

<style lang="stylus" scoped>

</style>

 ------------------------------------------------- end --------------------------------------------------

posted @ 2021-08-23 10:11  以深  阅读(63)  评论(0编辑  收藏  举报
TOP