#app.container.my-3
  .input-group.mb-3
    .input-group-prepend
      span#basic-addon1.input-group-text 待辦事項
    input.form-control(type='text' placeholder='準備要做的任務' v-model='newTodo' @keyup.enter='addTodo')
    .input-group-append
      button.btn.btn-primary(type='button' @click='addTodo') 新增
  .card.text-center
    .card-header
      ul.nav.nav-tabs.card-header-tabs
        template(v-for='(item, index) in visibilityList')
          li.nav-item(:key='index')
            a.nav-link(href='#' :class=" {'active' : visibility == item.value } " @click='visibility=item.value') {{item.name}}
    ul.list-group.list-group-flush.text-left
      template(v-for='(item) in filteredTodos')
        li.list-group-item(:key='item.id' @dblclick='editTodo(item)')
          .d-flex(v-if='item.id !== cacheTodo.id')
            .form-check
              input.form-check-input(:id='item.id' type='checkbox' @click='changeComplated(item.id)' v-model='item.completed')
              label.form-check-label(:for='item.id' :class="{'completed':item.completed}") {{ item.title }}
            button.close.ml-auto(type='button' aria-label='Close' @click='removeTodo(item)')
              span(aria-hidden='true') ×
          input.form-control(type='text' v-model='cacheTitle' v-if='item.id == cacheTodo.id' @keyup.esc='cancelEdit' @keyup.enter='doneEdit(item)')
    .card-footer.d-flex.justify-content-between
      | {{`還有${activeTodosLength}筆任務未完成`}}
      a(href='#' @click='cleanTodo') 清除所有任務
View Compiled
.completed {
	text-decoration: line-through;
}
View Compiled
var urlAPI = "https://eudora-hsj.github.io/Vue-practice/data/todolist.json"

var app = new Vue({
    el: "#app",
    data() {
        return {
            newTodo:'',
            todos: [],
            visibilityList: [
                { name: "全部", value: "all" },
                { name: "進行中", value: "active" },
                { name: "已完成", value: "completed" }
            ],
            visibility: 'all',
            cacheTodo: {},
            cacheTitle: '',
        }
    },
    created() {
        this.getList(urlAPI)
    },
    methods: {
        getList(url) {
            axios
                .get(url)
                .then((res) => {
                    this.todos = res.data.data
                })
                .catch((err) => {
                    console.log(err)
                })
        },
        addTodo() {
            let newTodoStr = this.newTodo.trim()
            if (!newTodoStr) {
                return
            }
            this.newTodo = ""
            let submitData = {
                id: Math.floor(Date.now()),
                title: newTodoStr,
                completed: false
            }
            this.todos.push(submitData)
        },
        removeTodo(item) {
            this.todos.splice(this.getIndex(item.id), 1)
        },
        editTodo(item) {
            this.cacheTodo = item
            this.cacheTitle = item.title
        },
        cancelEdit() {
            this.cacheTodo = {}
        },
        doneEdit(item) {
            item.title = this.cacheTitle
            this.cacheTitle = ""
            this.cacheTodo = {}
        },
        cleanTodo() {
            this.todos.splice(0, this.todos.length)
        },

        getIndex(id) {
            return this.todos.findIndex((el) => el.id == id)
        },
        changeComplated(id){
            let index = this.getIndex(id)
            this.todos[index].completed = !this.todos[index].completed
        }
    },
    computed: {
        filteredTodos() {
            let nowTab = this.visibility
            switch (nowTab) {
                case "all":
                    return this.todos.filter((item) => true)
                case "active":
                    return this.todos.filter((item) => !item.completed)
                case "completed":
                    return this.todos.filter((item) => item.completed)
            }
        },
        getNewKey() {
            return Math.max(...this.todos.map((el) => +el.id))
        },
        activeTodosLength() {
            return this.todos.filter((item) => !item.completed).length
        }
    }
})

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js
  2. https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js