JS 部分
为便于分析各个组件的相互作用与原理,故先从 JS 入手,而后再完善 HTML 部分
以下所有代码(除了 import 导包语句)都写在 todo.js
的 Page({})
对象内
数据 data
作为一款小而精的 todolist 小程序,我们仅需下方五个属性即可解决所有代码逻辑
inputValue
双向绑定输入框内容todoList
当前的 todo 表currentUndo
当前未完成的待办事项数allComplete
事项是否全部完成isFocus
用于搜索框自动获取焦点
data: {
inputValue: '',
todoList: [],
currentUndo: 0,
allComplete: false,
isFocus:false
},
onShow
由于我们没有在 app.json 中设置总标题,故在每个页面打开后,最好都在 onShow 钩子里面显式指定当前页面标题
// 进入此页面时,自动设置好标题
onShow: function () {
wx.setNavigationBarTitle({
title: 'Todo待办事项'
})
},
输入框双向绑定
由于微信小程序 功能太少太烂 ,特别是仅仅提供了单向绑定,而不像 vue 还有 v-model 执行双向绑定;
所有这里需要开发者自行实现双向绑定功能,具体流程即:监听输入框内容,内容发生改变触发回调函数修改对应的值,而通过单向绑定,实现值的动态变化
// 输入框双向绑定处理
inputChange(e) {
this.setData({
// e.detail.value可视为一种固定写法,可以从input类型的组件获取其值
inputValue: e.detail.value
})
},
保存与读取
代码不难理解,即我们需要在每次对 todo 进行增删后,都自带保存数据到本地,这样子下次开启小程序就可直接读取数据了
onLoad 回调保证页面加载时自动从内存读取 todolist 数据
// 保存todos到本地内存
saveTodos() {
wx.setStorageSync('todo_list', this.data.todoList)
},
// 加载todos列表
loadTodos() {
let todos = wx.getStorageSync('todo_list')
if (todos) {
let undo = todos.filter((item) => {
return !item.complete
}).length
this.setData({
todoList: todos,
currentUndo: undo
})
}
console.log(todos);
},
// 页面加载钩子调用loadTodos
onLoad() {
this.loadTodos()
},
添加新的待办事项
添加新待办事项的代码逻辑
// 将新的待办事项添加到栏内
addTodo() {
// 如果编辑框文本为空或者仅有空格的话,不予添加
if (!this.data.inputValue || !this.data.inputValue.trim()) return
// 每次都需要单独取出todolist然后push新的内容进去,最后setdata,你可以将其当做固定套路
let todos = this.data.todoList
todos.push({
title: this.data.inputValue,
complete: false
})
this.setData({
inputValue: "",
todoList: todos,
currentUndo: this.data.currentUndo + 1, // 添加新待办,未完成数+1
isFocus:true // 添加完一个待办后,自动令编辑框获得焦点,省去客户重复点击过程
})
// 每次修改完毕都必须保存一下!
this.saveTodos()
},
完成待办事项
请注意此处 complete 状态的设置:点击一次完成事项,再点击一次取消完成变为待办,此时就需要对 complete 做出判断,以动态增减 currentUndo
的数值量
// 点击完成单个事项
toggleTodo(e) {
let index = e.currentTarget.dataset.index
let todos = this.data.todoList
todos[index].complete = !todos[index].complete
this.setData({
todoList: todos,
currentUndo: this.data.currentUndo + (todos[index].complete ? -1 : 1),
})
this.saveTodos()
},
// 选中全部的待办事项
toggleAllTodos() {
this.data.allComplete = !this.data.allComplete
let todos = this.data.todoList
todos.forEach(i => {
i.complete = this.data.allComplete
})
this.setData({
todoList: todos,
currentUndo: this.data.allComplete ? 0 : todos.length
})
this.saveTodos()
},
删除待办事项
删除单个待办事项的方法为通过索引找到该 todo,并使用 splice
删去,之后更新数据即可
删除多个待办事项时需要配合 foreach
方法
// 删除单个待办事项
removeTodo(e) {
let index = e.currentTarget.dataset.index
let todos = this.data.todoList
let remove = todos.splice(index, 1)[0]
this.setData({
todoList: todos,
currentUndo: this.data.currentUndo - (remove.complete ? 0 : 1)
})
this.saveTodos()
},
// 删除选中项
removeTodos(e) {
let todos = this.data.todoList
let remain = []
todos.forEach(i => {
if (!i.complete) remain.push(i)
})
this.setData({
todoList: remain
})
this.saveTodos()
// wx自带的手机振动接口,vibrateShort表示振动15ms
wx.vibrateShort()
},
WXML
顶部输入框
这里涉及到的 t-input 配置项可以自行前往官网查询 API,这里不做过多解释
bind:change、bind:blur、bind:enter
分别表示 内容改变、是否获取焦点、按下回车键
后的回调函数
对于 t-button
,如果他在 t-input
内部,则需使用 slot="suffix"
指定插槽来安放该 button
<!-- 顶部添加栏 -->
<view class="todo-input">
<t-input
value="{{inputValue}}"
style="border-radius: 12rpx;"
clearable
placeholder="请输入事项名称"
confirm-type="done"
bind:change="inputChange"
bind:blur="isFocus=!isFocus"
bind:enter="addTodo"
focus="{{isFocus}}"
>
<t-button
wx:if="{{inputValue}}"
slot="suffix"
theme="light"
size="small"
bindtap="addTodo"
>
添加
</t-button>
</t-input>
</view>
主体
block
配以 wx:if
,实现状态页显示:当待办事项列表为空时,动态判断并显示 404 页面
<!-- 当todo列表存在数据时,渲染该页面 -->
<block wx:if="{{todoList.length}}">
<view class="todo-control">
<image bindtap="toggleAllTodos" src="../../image/pages/all.png"></image>
<text wx:if="{{currentUndo}}">待完成任务 {{currentUndo}}</text>
<image
bindtap="removeTodos"
src="../../image/pages/delete.png"
wx:if="{{todoList.length>currentUndo}}"
></image>
</view>
<view class="todo-itembox">
<view
class="todo-items {{item.complete?'comp':''}}"
wx:for="{{todoList}}"
wx:key="index"
data-index="{{index}}"
bindtap="toggleTodo"
>
<icon
class="checkbox"
type="{{ item.complete ? 'success' : 'circle' }}"
></icon>
<text class="title">{{ item.title }}</text>
<icon
class="remove"
type="clear"
size="16"
catchtap="removeTodo"
data-index=" {{index}} "
/>
</view>
</view>
</block>
<!-- 当todo列表为空时,渲染该页面 -->
<block wx:else>
<view class="todo-empty">
<image src="../../image/state/no-data.png"></image>
<view style="color: gray;">当前还没有待办事项哦~</view>
</view>
</block>
回到顶部按钮
<!-- 回到顶部悬浮按钮 -->
<!-- 判断当且仅当列表项多于6个时,才会显示该悬浮按钮 -->
<t-fab wx:if="{{todoList.length>6}}" icon="arrow-up" bind:click="fabBack2Top" />
CSS 部分由于不方便表述,故留到文末以源码的形式展现给大家
完整代码
JS
import Message from "tdesign-miniprogram/message/index";
// pages/todo/todo.js
Page({
data: {
inputValue: "",
todoList: [],
currentUndo: 0,
allComplete: false,
isFocus: false,
},
// 进入此页面时,自动设置好标题
onShow: function () {
wx.setNavigationBarTitle({
title: "Todo待办事项",
});
},
// 输入框双向绑定处理
inputChange(e) {
this.setData({
inputValue: e.detail.value,
});
},
// 保存todos到本地内存
saveTodos() {
wx.setStorageSync("todo_list", this.data.todoList);
},
// 加载todos列表
loadTodos() {
let todos = wx.getStorageSync("todo_list");
if (todos) {
let undo = todos.filter((item) => {
return !item.complete;
}).length;
this.setData({
todoList: todos,
currentUndo: undo,
});
}
console.log(todos);
},
// 页面加载钩子调用loadTodos
onLoad() {
this.loadTodos();
},
// 将新的待办事项添加到栏内
addTodo() {
if (!this.data.inputValue || !this.data.inputValue.trim()) return;
let todos = this.data.todoList;
todos.push({
title: this.data.inputValue,
complete: false,
});
this.setData({
inputValue: "",
todoList: todos,
currentUndo: this.data.currentUndo + 1,
isFocus: true,
});
this.saveTodos();
},
// 悬浮按钮回到顶部
fabBack2Top() {
wx.pageScrollTo({
duration: 500,
scrollTop: 0,
});
},
// 点击完成单个事项
toggleTodo(e) {
let index = e.currentTarget.dataset.index;
let todos = this.data.todoList;
todos[index].complete = !todos[index].complete;
this.setData({
todoList: todos,
currentUndo: this.data.currentUndo + (todos[index].complete ? -1 : 1),
});
this.saveTodos();
},
// 选中全部的待办事项
toggleAllTodos() {
this.data.allComplete = !this.data.allComplete;
let todos = this.data.todoList;
todos.forEach((i) => {
i.complete = this.data.allComplete;
});
this.setData({
todoList: todos,
currentUndo: this.data.allComplete ? 0 : todos.length,
});
this.saveTodos();
},
// 删除单个待办事项
removeTodo(e) {
let index = e.currentTarget.dataset.index;
let todos = this.data.todoList;
let remove = todos.splice(index, 1)[0];
this.setData({
todoList: todos,
currentUndo: this.data.currentUndo - (remove.complete ? 0 : 1),
});
this.saveTodos();
},
// 删除选中项
removeTodos(e) {
let todos = this.data.todoList;
let remain = [];
todos.forEach((i) => {
if (!i.complete) remain.push(i);
});
this.setData({
todoList: remain,
});
if (this.data.currentUndo === 0) {
Message.success({
context: this,
content: "完成所有任务,休息一下吧!",
duration: 2000,
offset: [20, 32],
closeBtn: true,
});
}
this.saveTodos();
wx.vibrateShort();
},
});
WXML
<view class="todo-container">
<!-- 顶部添加栏 -->
<view class="todo-input">
<t-input
value="{{inputValue}}"
style="border-radius: 12rpx;"
clearable
placeholder="请输入事项名称"
confirm-type="done"
bind:change="inputChange"
bind:blur="isFocus=!isFocus"
bind:enter="addTodo"
focus="{{isFocus}}"
>
<t-button
wx:if="{{inputValue}}"
slot="suffix"
theme="light"
size="small"
bindtap="addTodo"
>
添加
</t-button>
</t-input>
</view>
<!-- 当todo列表存在数据时,渲染该页面 -->
<block wx:if="{{todoList.length}}">
<view class="todo-control">
<image bindtap="toggleAllTodos" src="../../image/pages/all.png"></image>
<text wx:if="{{currentUndo}}">待完成任务 {{currentUndo}}</text>
<image
bindtap="removeTodos"
src="../../image/pages/delete.png"
wx:if="{{todoList.length>currentUndo}}"
></image>
</view>
<view class="todo-itembox">
<view
class="todo-items {{item.complete?'comp':''}}"
wx:for="{{todoList}}"
wx:key="index"
data-index="{{index}}"
bindtap="toggleTodo"
>
<icon
class="checkbox"
type="{{ item.complete ? 'success' : 'circle' }}"
></icon>
<text class="title">{{ item.title }}</text>
<icon
class="remove"
type="clear"
size="16"
catchtap="removeTodo"
data-index=" {{index}} "
/>
</view>
</view>
</block>
<!-- 当todo列表为空时,渲染该页面 -->
<block wx:else>
<view class="todo-empty">
<image src="../../image/state/no-data.png"></image>
<view style="color: gray;">当前还没有待办事项哦~</view>
</view>
</block>
<!-- 回到顶部悬浮按钮 -->
<!-- 判断当且仅当列表项多于6个时,才会显示该悬浮按钮 -->
<t-fab
wx:if="{{todoList.length>6}}"
icon="arrow-up"
bind:click="fabBack2Top"
/>
</view>
WXSS
.todo-container {
margin: 0;
padding: 0;
width: 100vw;
min-height: 100vh;
background-color: #ededed;
display: flex;
flex-direction: column;
position: relative;
}
.todo-input {
margin: 12rpx 32rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.todo-empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.todo-empty image {
width: 450rpx;
height: 450rpx;
}
/* 悬浮按钮 */
.fab {
position: absolute;
right: 0;
bottom: 0;
}
/* 多选操纵栏 */
.todo-control {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 80rpx;
margin: 12rpx 32rpx;
}
.todo-control image {
height: 70rpx;
width: 70rpx;
}
/* todo项目展示 */
.todo-items {
border-radius: 8rpx;
height: 120rpx;
background-color: white;
margin: 10rpx 32rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.comp {
background-color: lightgray;
}
.todo-items .checkbox {
margin-left: 24rpx;
}
.todo-items .title {
min-width: 70%;
max-width: 70%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.todo-items .remove {
margin-right: 24rpx;
}