最近對vue很感興趣,趁閒暇時間,模仿了wunderlist裏面的部分功能,編寫了先後端分離的基於vue技術棧和express的todolist小項目。寫這篇博文來總結思考下。項目所在github,能夠自行參考克隆。javascript
本人博客css
整個項目最終作成的樣子以下:html
你們都看到了,總體實現的功能仍是比較簡單的。因爲對express也很感興趣,就乾脆本身動手作了全棧。另外說一下:這個項目只是本身摸索vue與express過程當中,作出的成果,若是有哪一個地方不對的,還請大神多多指教。前端
整個todolist界面分爲左側的目錄分類和右側的list。用戶切換不一樣的目錄能夠對應到相應的list任務中,而且在該任務中可以添加list和刪除list,也可以標記已完成與未完成。這些看上去都很簡單,可是裏面存在了挺多小細節的,我認爲,做爲一個新手,尤爲是對vue新,對express新又對他們很感興趣的新手,拿這個項目來練手我的以爲很合適。vue
很少說廢話了,先來看看個人項目目錄與大體的介紹吧。java
├── README.md //這裏是readme說明文檔 ├── node_modules //一些依賴在這裏 ├── build │ ├── build.js │ └── dev.js ├── db //數據庫相關的東西放在這裏 │ └── dbConfig.js //數據庫配置文件 ├── dist //webpack編譯後的目標文件夾 ├── package.json //這個就不說了,是個前端都懂 ├── server //服務器端相關的文件都在這裏 │ └── app.js //server服務,後臺服務入口文件 ├── src //前端源文件都在這裏 │ ├── App.vue //vue頂級組件,包含了vue-router │ ├── components //各個子組件 │ │ ├── common //包含了頁面的公共模塊,好比header,footer等 │ │ ├── menu-item.vue //左側菜單欄組件 │ │ ├── search.vue //搜索框組件 │ │ └── todo-list.vue //list組件 │ ├── config //一些前端配置的東西能夠放在這裏 │ ├── directives //vue的一些指令封裝能夠放在這裏 │ ├── filters //vue的一些過濾器能夠放在這裏 │ ├── images //放置圖片 │ │ ├── Shapes.jpg │ │ ├── article.jpeg │ │ └── avatar.png │ ├── less //公共樣式less相關放在這裏 │ │ ├── common.less │ │ ├── fonts │ │ ├── index.less │ │ ├── lessfont │ │ ├── mixin.less │ │ ├── reset.less │ │ └── variable.less │ ├── main.js //前端入口文件 │ ├── pages //放置不一樣的頁面,本項目只有一個頁面,因此暫定只有一個 │ │ └── index.vue │ ├── route.js //路由配置文件 │ └── store //vuex相關邏輯放在這裏 │ ├── actions.js //actions相關 │ ├── getters.js //getter相關 │ ├── index.js //頂端vuex設置,入口文件 │ ├── modules //放置模塊 │ ├── mutations.js //mutation相關 │ ├── plugins.js //插件相關 │ └── state.js //state相關 ├── webpack.config.base.js //webpack基本配置 ├── webpack.config.dev.js //開發環境配置 └── webpack.config.prod.js //線上環境配置,但沒有測試過也沒有怎麼研究,能夠暫時忽略
下面來分先後端兩個模塊大體講解一下:node
首先要準備mysql數據庫服務,我用MYSQLWorkbench客戶端界面新建數據庫,初始化庫表信息,固然你也能夠不用圖形界面,直接用命令行。在dbConfig.js
中配置好數據庫設置mysql
module.exports = { host : 'localhost', user : 'your user name default root', password : 'your password', database : 'taskiller' }
本項目中,數據庫數據表的新建,並無寫在server服務中,在實際的項目中應該有個自動化的腳本自動建立你須要的數據表和須要的字段信息。由於是當成練手項目作的,因此一切都從簡了,把項目用到的數據庫和數據表都事先創建好了。個人數據庫名是taskiller
,須要在這個數據庫中,建兩個表:todo_list
和menu
,todolist
用來存儲list信息,menu
用來存儲目錄信息。示例:
menu表webpack
+----+--------+----------+ | id | name | selected | +----+--------+----------+ | 1 | sdd | 0 | +----+--------+----------+
todo_list表ios
+--------------+----+------+---------+ | text | id | done | menu_id | +--------------+----+------+---------+ | sdfs | 46 | 0 | 1 | | this is life | 47 | 0 | 4 | +--------------+----+------+---------+
接下來就是後端服務文件app.js
了,看代碼說話吧:
const path = require('path') const express = require('express') const mysql = require('mysql'); const dbConfig = require('../db/dbConfig'); const bodyParser = require('body-parser') const insertMenu = 'INSERT INTO menu SET ?' const getMenu = 'SELECT * FROM menu WHERE id = ?' const getAllMenu = 'SELECT * FROM menu' const getTodolist = 'SELECT * FROM todo_list WHERE menu_id = ?' const insertTodolist = 'INSERT INTO todo_list SET ?' const deleteTodo = 'DELETE from todo_list WHERE id = ?' const updateTodolist = 'UPDATE todo_list SET done = ? WHERE id = ?' let menus; //全部menu列表緩存 const app = express(); app.use(bodyParser()); //鏈接數據庫 let connection = mysql.createConnection(dbConfig); app.all('*', function(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS"); res.header("X-Powered-By",' 3.2.1') res.header("Content-Type", "application/json;charset=utf-8"); next(); }); //添加目錄 app.post('/menu/add', function (req, res, next) { let reqParam = req.body; connection.query(insertMenu, reqParam, function(error, results, fields) { if(error) throw error; console.log(results, fields) }) res.sendStatus(200); next() }); //獲得全部目錄 app.get('/menu/get', function(req, res, next) { connection.query(getAllMenu, function(error, results, fields) { if(error) throw error; menus = results; res.json(results); next() }) }) //獲得指定id的目錄 app.get('/menu/get/:id', function(req, res, next) { console.log('ID:', req.params.id); connection.query(getMenu, req.params.id, function(error, results, fields) { if(error) throw error; res.json(results); }) }) //根據目錄獲取todolist app.get('/todolist/get/:id', function(req, res, next) { connection.query(getTodolist, req.params.id, function(error, results, fields) { if(error) return error; res.json(results); }) }) //添加todolist到數據庫中 app.post('/todolist/add', function(req, res, next) { //text,done, menu_id let reqParam = { "text": req.body.data.text, "done": false, "menu_id": req.body.data.curMenu }; var insertId; connection.query(insertTodolist, reqParam, function(error, results, fields) { if(error) throw error; insertId = results.insertId; reqParam.id = insertId; res.json(reqParam) }) }) //刪除todolist app.post('/todolist/delete', function(req, res, next) { let reqParam = req.body.id connection.query(deleteTodo, reqParam, function(error, results, fields) { if(error) throw error; console.log(results, fields) }) res.sendStatus(200); }) //改變todolist狀態 app.post('/todolist/toggle', function(req, res, next) { let reqParam = req.body; console.log(reqParam) connection.query(updateTodolist, [!reqParam.done, reqParam.id], function(error, results, fields) { if(error) throw error; console.log(results) res.sendStatus(200); }) }) app.listen(3001, function() { console.log('listening on port 3001') }) connection.connect(function(err) { if (err) { console.error('error connecting: ' + err.stack); return; } console.log('connected as id ' + connection.threadId); });
很簡單,全部的接口功能應該都是一目瞭然。由於筆者主要鍛鍊的仍是vue相關的,express只是有興趣略微帶過,所以沒有考慮很複雜很完善的一些邏輯。有幾個注意事項:
由於前端的地址端口是3000,後端server的端口又設定了3001,這就涉及到了跨域,所以我加了這段代碼,來解決跨域的問題。
app.all('*', function(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS"); res.header("X-Powered-By",' 3.2.1') res.header("Content-Type", "application/json;charset=utf-8"); next(); });
其實這裏仍是有漏洞的,坐等高手指出來(微笑臉)
後臺express沒有用express-generator生成一個完整的架構。筆者以前嘗試過用這種一鍵生成的工具快速搭建後臺環境,可是這樣就都用現成的,好多東西概念就會很是模糊,不太好掌握一些技術細節,也不會很透徹理解這樣寫的架構究竟是爲何,爲何不採用其餘架構方式?因此,筆者決定本身純手寫後臺的server,這個最初的版本是寫的最簡單的版本,等後期再深刻研究express的時候,再把這個雛形向着可擴展性和模塊化發展。不過已經對express很熟的同窗徹底能夠不照着我這個小白的寫法寫~
前端剛開始我遇到的門檻就是webpack一些配置,網上的教程真的是五花八門,因爲本人的目的是學習vue的,並非搗鼓webpack,因此,在webpack配置方面並無花不少的時間研究,也是在網上找教程,慢慢摸索倒騰出來的。不過通過此次倒騰我認識到,我有必要研究下webpack的一些東西了。否則配置個東西,太痛苦,而且前端技術突飛猛進,網上的教程五花八門,有老舊版本的,有新的版本的,很容易讓人摸不着頭腦,建議仍是去官網學習比較好。後期我研究了再寫博文總結下經驗。
這裏先貼一下個人webpack配置,有些地方作了簡要的註釋。
webpack.config.base.js
const webpack = require('webpack'); const path = require("path"); const fs = require("fs"); const ExtractTextPlugin = require("extract-text-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const autoprefixer = require('autoprefixer'); const PATHS = { src: path.resolve(__dirname, './src'), dist: path.resolve(__dirname, './dist') } module.exports = { entry: { app: './src/main.js', // 整個SPA的入口文件, 一切的文件依賴關係從它開始 vendors: ['vue', 'vue-router'] // 須要進行單獨打包的文件 }, output: { path: PATHS.dist, filename: 'js/[name].js', publicPath: '/dist/', // 部署文件 相對於根路由 chunkFilename: 'js/[name].js' // chunk文件輸出的文件名稱 具體格式見webpack文檔, 注意區分 hash/chunkhash/contenthash 等內容, 以及存在的潛在的坑 }, devtool: '#eval-source-map', // 開始source-map. 具體的不一樣配置信息見webpack文檔 module: { rules: [{ test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js/, loader: 'babel-loader', exclude: /node_modules/ }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader?limit=10240&name=images/[name].[ext]' }, { test: /\.less/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [ 'css-loader', 'less-loader' ] }) }, { test: /\.css/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [ 'css-loader' ] }) }, { test: /\.(eot|svg|ttf|woff|woff2|png)\w*/, loader: 'file-loader' } ] }, resolve: { alias: { 'vue$': 'vue/dist/vue.common.js', 'components': path.join(__dirname, 'src/components'), // 定義文件路徑, 加速打包過程當中webpack路徑查找過程 'lib': path.join(__dirname, 'src/lib'), 'less': path.join(__dirname, 'src/less'), 'filters': path.join(__dirname, 'src/filters'), 'directives': path.join(__dirname, 'src/directives'), }, extensions: ['.js', '.less', '.vue', '*', '.json'] // 能夠不加後綴, 直接使用 import xx from 'xx' 的語法 }, plugins: [ new HtmlWebpackPlugin({ // html模板輸出插件 title: 'task kill', template: `${PATHS.dist}/template/index.ejs`, inject: 'body', filename: `${PATHS.dist}/pages/index.html` }), new ExtractTextPlugin({ // css抽離插件,單獨放到一個style文件當中. filename: `css/style.css`, allChunks: true, disable: false }), // 將vue等框架/庫進行單獨打包, 並輸入到vendors.js文件當中 // 這個地方commonChunkPlugin一共會輸出2個文件, 第二個文件是webpack的runtime文件 // runtime文件用以定義一些webpack提供的全局函數及須要異步加載的chunk文件 // 具體的內容能夠看我寫的blog // [webpack分包及異步加載套路](https://segmentfault.com/a/1190000007962830) // [webpack2異步加載套路](https://segmentfault.com/a/1190000008279471) new webpack.optimize.CommonsChunkPlugin({ names: ['vendors', 'manifest'] }) ] }
webpack.config.dev.js
const merge = require('webpack-merge'); module.exports = merge(require('./webpack.config.base'), { devServer: { proxy: { '/api': { target: 'http://localhost:3001', changeOrigin: true, secure: false, pathRewrite: { "^/api": "" } } } } })
webpack.config.prd.js就不貼了,此次我也沒有用上,你們有興趣能夠直接去github裏看。
在這裏不得不感謝vuex,這個東西對開發效率的提高真的頗有幫助,vuex不熟悉的童鞋能夠去官網查閱,建議既然學習了vue了,vuex必不可少,真的會節省你不少的開發時間。這裏我就作個簡要的介紹:
vuex是一個專爲vue開發的狀態管理模式,它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式變化。
這個狀態自管理應用包含如下幾個部分:
state,驅動應用的數據源;
view,以聲明方式將state映射到視圖;
actions,響應在view上的用戶輸入致使的狀態變化。
好比如下的單一數據流示意圖:
可是,有過組件編寫經驗的童鞋應該知道,當咱們的應用遇到多個組件共享狀態時,單向數據流的簡潔性很容易被破壞:
多個視圖依賴於同一狀態。
來自不一樣視圖的行爲須要變動同一狀態。
對於問題一,傳參的方法對於多層嵌套的組件將會很是繁瑣,而且對於兄弟組件間的狀態傳遞無能爲力。對於問題二,咱們常常會採用父子組件直接引用或者經過事件來變動和同步狀態的多份拷貝。以上的這些模式很是脆弱,一般會致使沒法維護的代碼。
所以,誕生了vuex,用來把組件的共享狀態抽取出來,以一個全局單例模式管理。在這種模式下,組件樹構成了一個巨大的「視圖」,無論在樹的哪一個位置,任何組件都能獲取狀態或者觸發行爲!
另外,經過定義和隔離狀態管理中的各類概念並強制遵照必定的規則,代碼也會變得更結構化且易維護。
來看看這張經典的圖例:
大體的就介紹到這裏啦,須要更加深刻的童鞋能夠移步官網。下面針對本項目的邏輯,介紹下我設計的vuex。
因爲目前實現的邏輯仍是較爲簡單,所以,只有涉及了3個state:
export const state = { todos: [], //當前選中目錄curMenu對應的todolist curMenu: {}, //選中的目錄 menus: [] //全部目錄列表 }
目錄對應的todo列表的切換,我採用的方式是:curMenu
只要一有變更就會向後端發送請求,後臺返回該目錄下對應的全部todo列表,更新到todos。所以,這裏只設計了一個todos狀態就好了。
mutation只能同步,沒法異步,所以mutations.js
只設計狀態的改變。此處根據交互有這幾處涉及到的狀態改變:
向todos中添加todo
在todos中刪除選中的todo
更新選中todo的完成狀態
設置當前目錄對應的todos數組
設置全部的目錄列表menus
設置當前選中的目錄curMenu
對應的js代碼以下:
export const mutations = { //添加todo addTodo(state, {todo}) { state.todos.push(todo) }, //刪除todos deleteTodo(state, {todo}) { state.todos.splice(state.todos.indexOf(todo), 1); }, //設置當前的todos setTodo(state, {todos}) { state.todos = todos }, //切換todo的完成狀態 toggleTodo(state, {todo}) { todo.done = !todo.done; }, //左側menu切換,設置當前menu值 setCurMenu(state, {menu}) { state.curMenu = menu; }, //設置menu值, getMenu(state, {menus}) { state.menus = menus; } }
因爲須要後臺存儲一些todo列表的狀態,須要將一些動做的變更告知後臺,後臺更新相應的數據庫信息,須要添加actions。
這裏要注意:actions.js
主要放置一些與後臺交互相關的操做,而mutations.js
只用做狀態的改變。
仔細看看交互,會發現這裏存在如下幾個動做:
獲取全部的目錄,即得到menus列表
切換menu時,向後臺獲取對應的todolist,並更新對應todos列表
用戶添加list時,向後臺發送請求存儲在數據庫中
用戶刪除list時,向後臺發送請求將數據庫中該條記錄刪除
用戶改變todolist中的某個list的狀態時,向後臺發送請求更新數據庫中該條記錄的done
字段
import axios from 'axios'; const host = 'http://localhost:3001' //獲取全部的menu export const getMenus = ({commit}) => { axios.get(host + '/menu/get') .then((response) => { var data = response.data; var initMenu data.forEach(function(item) { if(item.selected) { initMenu = item } }) commit('getMenu', {menus: data}); //提交mutation,初始化menus列表 commit('setCurMenu', {menu: initMenu}); //提交mutation,初始化curMenu。 axios.get(host + '/todolist/get/'+initMenu.id) .then((response) => { var data = response.data; commit('setTodo', {todos: data}) //根據初始化的curMenu獲取todolist,並提交mutation更新todos列表 }) .catch( error => { console.log(error) }) }) .catch( error => { console.log(error) }) } //確切的講是getCurTodoList,獲得當前menu對應的todolist export const setCurMenu = ({commit}, menuData) => { axios.get(host + '/todolist/get/'+menuData.id) .then((response) => { var data = response.data; commit('setTodo', {todos: data}) //用數據庫返回的數據提交mutation,更新todo列表 commit('setCurMenu', {menu: menuData.menu}) //更新當前目錄,提交mutation來更新curMenu值 }) .catch( error => { console.log(error) }) } //添加todo export const addTodo = ({commit}, data) => { axios.post(host + '/todolist/add', { data: data, }) .then((response) => { commit('addTodo', {todo: response.data}) // 提交mutation,向todos中添加數據庫返回的記錄 }) .catch( error => { console.log(error) }) } //刪除todo export const deleteTodo = ({commit}, todo) => { axios.post(host + '/todolist/delete', { id: todo.todo.id, }) .then((response) => { commit('deleteTodo', todo) //提交mutation,在todos中刪除該條記錄 }) .catch( error => { console.log(error) }) } //完成與未完成的切換 export const toggleTodo = ({commit}, todo) => { axios.post(host + '/todolist/toggle', { id: todo.todo.id, done: todo.todo.done }) .then((response) => { console.log(response) commit('toggleTodo', {todo: todo.todo}) //提交mutation,更新該條todo的完成狀態 }) .catch( error => { console.log(error) }) }
細心的同窗會發現,個人界面裏面分門別類的顯示了已完成和未完成的類別,所以須要經過getters來根據todos的數據得到對應的數據
export const doneTodos = state => { return state.todos.filter(item => item.done) } export const undoneTodos = state => { return state.todos.filter( item => !item.done) }
這裏我就很少贅述了,都是組件相關的概念,要細講的話概念點就太多了,看不懂的建議你們去vue官網學習相關概念再來看。
index.vue
<template> <article class="post-page"> <div class="g-left"> <menus></menus> </div> <div class="g-right"> <input class="adds" type="text" v-model="addlists" @keyup.enter="addList($event)" placeholder="添加任務..."> <todo-list class="todo" v-for="(todo, index) in undoneTodos" :todo="todo" :key="index"></todo-list> <div class="title" @click="isShowDone = !isShowDone"> {{isShowDone ? '隱藏已完成任務' : '顯示已完成任務'}} <span class="subtitle">共{{doneTodos.length}}項</span></div> <todo-list v-show="isShowDone" class="todo" v-for="(todo, index) in doneTodos" :todo="todo" :key="index + 'done'"></todo-list> </div> </article> </template> <script> import TodoList from 'components/todo-list.vue' import Menus from 'components/common/menus.vue' import { mapState } from 'vuex' import {mapGetters} from 'vuex' const COMPONENT_NAME = 'post-page'; export default { name: COMPONENT_NAME, data() { return { addlists: '', isShowDone: false } }, computed: { ...mapState([ 'todos', 'curMenu' ]), ...mapGetters([ 'doneTodos', 'undoneTodos' ]) }, methods: { addList(e) { var text = this.addlists var curMenu = this.curMenu.id this.$store.dispatch('addTodo', {text: text, curMenu: curMenu}) this.addlists = ''; }, }, components: { TodoList, Menus } } </script> <style lang="less"> .post-page { width: 100%; } .adds{ width: 100%; border: 1px solid #dedede; margin-bottom: 40px; margin-left: auto; margin-right: auto; height: 32px; line-height: 32px; border-radius: 3px; padding-left: 10px; } .todo{ text-align: left; } .title{ width: 300px; height: 32px; line-height: 32px; text-align: left; cursor: pointer; .subtitle{ color: #999; font-size: 12px; } } </style>
這裏的mapState
和mapGetters
等都在vuex官網裏面有介紹,前面的三點...
是es6
的語法對象展開符,不懂的童鞋可google一下,網上一堆教程。
咱們能夠看到,這個頁面我用到了menus
組件和todo-list
組件,這裏我遇到過一個坑,vue文件的模板template
中的元素只能有一個根節點,不能有多個根節點,你們寫的時候在只在頂層寫一個標籤就行了。
menus.vue
<template> <div class="menu"> <menu-item-list v-for="menu in menus" :item="menu" ></menu-item-list> </div> </template> <script> import MenuItemList from 'components/menu-item.vue' import {mapActions} from 'vuex' import {mapState} from 'vuex' const COMPONENT_NAME = 'menu'; export default { name: COMPONENT_NAME, data() { return { } }, created() { this.getMenus(); }, components: { MenuItemList }, computed: { ...mapState(['menus']) }, methods: { ...mapActions(['getMenus']) } } </script> <style lang="less"> .menu { width: 100%; } </style>
menu-item.vue
<template> <div class="menu-list" @click="setCurMenu(item)" :class="{selected: curMenu === item}"> <div> <i class="fa fa-list-ul" aria-hidden="true"></i> <span>{{item.name}}</span> <span v-show="!item.selected" class="tips"></span> <i v-show="curMenu === item" class="fa fa-pencil-square-o tips" aria-hidden="true"></i> </div> </div> </template> <script> const COMPONENT_NAME = 'menu-item-list'; import {mapState} from 'vuex' export default { name: COMPONENT_NAME, data() { return { } }, computed: { ...mapState([ 'curMenu', ]), }, props: ['item'], methods: { setCurMenu(menu) { this.$store.dispatch('setCurMenu', {id: this.item.id, menu: menu}) } } } </script> <style lang="less"> .menu-list { width: 100%; padding-left: 20px; cursor: pointer; &:hover{ background: #29A9FF; div{ color: #fff; } } &.selected{ background: #29A9FF; div{ color: #fff; } } div{ height: 38px; line-height: 38px; color: #333; } i{ display: inline-block; height: 38px; line-height: 38px; } .tips{ float: right; margin-right: 20px; } } </style>
todo-list.vue
<template> <div class="list"> <input type="checkbox" :checked="todo.done" @change="toggleTodo({todo: todo})"> <span>{{todo.text}}</span> <span class="delete fa fa-times-circle" aria-hidden="true" @click="deleteTodo({todo: todo})"></span> </div> </template> <script> import { mapActions} from 'vuex' const COMPONENT_NAME = 'todo-list'; export default { name: COMPONENT_NAME, props: ['todo'], methods: { ...mapActions([ 'deleteTodo', 'toggleTodo' ]) } } </script> <style lang="less" coped> .list{ width: 300px; height: 32px; line-height: 30px; border: 1px solid #dedede; margin-bottom: 20px; } .delete{ float: right; margin-top: 8px; margin-right: 10px; cursor: pointer; color: #999; } </style>
至此,主要的功能結構都講的差很少了,有哪裏講的不清楚的地方,還望指出來,互相學習。
不得不說,vue框架用來來真的不錯,你們在開發的時候,記住:要從數據的角度思考問題,一切就會變得如此簡單。
這裏記錄下接下來要添加的功能,若是有踩坑會繼續帶來博文分享互相交流學習下:
頂部路由添加user登陸信息
頂部單獨闢出一個路由顯示已完成任務或其餘主題
左側menu欄目添加新建目錄功能,其實後端接口已經寫好,前端須要加一下
左側每一個目錄須要有右鍵功能,右鍵彈出項目暫定: 重命名、刪除
右側添加任務的界面要改的好一點,樣式細節須要調整
右側輸入框目前是點擊回車以後會自動添加一個項目,可是在中文輸入法時直接按回車切換英文時存在bug,會致使list存在空白的狀況,這個要處理一下
每一個list是否須要能夠編輯待定
左側目錄最右端顯示有幾條todolist功能添加