开源小程序-高仿微软待办To-Do源码分析
发布于 2 年前 作者 aliang 4479 次浏览 来自 分享

项目简介

此项目基于小程序云开发,你不需要自己搭建服务器环境。产品原型参考自Microsoft To-Do(微软待办)

项目界面

待办小程序:包含了待办首页、待办列表、待办详情、重要事项、待办列表,包含了待办小程序的所有基础功能。

待办首页:

点击右上角菜单可以呼出重要事项和代办列表入口:

待办详情

重要事项

待办列表

代码分析

前端代码亮点

可以看到以上三个页面:待办首页、重要待办、待办列表都是很多布局都是重复的,所以在这里作者采用了把相同布局封装成自定义组件的方式。

待办首页:

<!--index.wxml-->
<view class="page">
    <view class="page-top">
        <view class="top-left">
            <view class="day-text">我的一天</view>
            <view class="day-date">
                {{currentDate}}
            </view>
        </view>
        <view class="top-right">
            <view bindtap="openMenuPopup">
                <van-icon name="wap-nav" />
            </view>
        </view>
    </view>
    <view class="page-content">
        <todo-list loading="{{loading}}" empty-text="您今天还没有任务~" todo-list="{{todoList}}"></todo-list>
    </view>
    <!-- todo input -->
    <view class="todo-input-wrapper fixed-bottom">
        <todo-input bind:success="onAddTodoSuccess"></todo-input>
    </view>

    <!-- 右侧菜单 -->
    <van-popup show="{{ showMenuPopup }}" position="right" custom-style="height: 100%;" bind:close="closeMenuPopup">
        <side-menu bind:getuserinfo="onGetUserInfo" bind:click-menu-list-item="clickMenuItem" user-info="{{userInfo}}" subscript="{{menuSubscript}}" />
    </van-popup>
        <!-- 在页面内添加对应的节点 -->
        <van-notify id="van-notify" />
</view>

重要事项:

<!--miniprogram/pages/important/important.wxml-->
<view class="page important-wrapper">
     <view class="page-content">
        <todo-list loading="{{loading}}" todo-list="{{todoList}}"></todo-list>
    </view>
     <!-- todo input -->
    <view class="todo-input-wrapper fixed-bottom">
        <todo-input bind:success="onAddTodoSuccess" page-type="{{1}}"></todo-input>
    </view>
</view>

待办列表

<!--miniprogram/pages/important/important.wxml-->
<view class="page important-wrapper">
     <view class="page-content">
        <todo-list loading="{{loading}}" todo-list="{{todoList}}"></todo-list>
    </view>
     <!-- todo input -->
    <view class="todo-input-wrapper fixed-bottom">
        <todo-input bind:success="onAddTodoSuccess" page-type="{{2}}"></todo-input>
    </view>
</view>

自定义组件的好处有以下两点,在看看封装的自定义组件。

  • 可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;
  • 可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。

todolist
wxml

<!--components/todolist/todolist.wxml-->
<view class="todo-lsit-wrapper">
    <view wx:if="{{loading}}">
        <van-skeleton row="6" />
    </view>
    <view wx:else>
        <view wx:if="{{todoList.length > 0}}" class="todo-list-content">
            <todo-item bind:checkboxchange="checkboxChange" bind:clicktodoitem="clickTodoItemHandle" bind:clicktodoright="clickTodoItemRight" todo="{{item}}" data-todo="{{item}}" wx:for="{{todoList}}" wx:key="_id" />
        </view>
        <view wx:else class="no-data">
            <image src="../../images/no-data.png"></image>
            <view style="margin-top:20rpx;">
                {{emptyText}}
            </view>
        </view>
    </view>

</view>

wxss

/* components/todolist/todolist.wxss */
.todo-lsit-wrapper{
    padding:0 20rpx;
    margin-top: 20rpx;
}

.no-data{
    color: #fff;
    text-align: center;
    font-size: 12px;
}

js

// components/todolist/todolist.js
Component({
    /**
     * 组件的属性列表
     */
    properties: {
        todoList: {
            type: Array,
            value: []
        },
        loading: {
            type: Boolean,
            value: true
        },
        emptyText: {
            type: String,
            value: '数据是空的~'
        }
    },

    /**
     * 组件的初始数据
     */
    data: {

    },

    /**
     * 组件的方法列表
     */
    methods: {
        checkboxChange(event) {
            console.log('ev', event)
        },
        clickTodoItemHandle(event) {
            console.log(event)
        },
        clickTodoItemRight(event) {
            console.log(event)
        }
    }
})

json

{
    "component": true,
    "usingComponents": {
        "todo-item": "../todo-item/TodoItem"
    }
}

在list里面包含了item组件,接下来我们来看到item的具体代码。
wxml

<!--components/todo-item/TodoItem.wxml-->
<view class="todo-item-wrapper">
    <view class="todo-item-content">
        <view class="todo-check">
            <van-checkbox bind:change="onChange" value="{{ todo.done }}"></van-checkbox>
        </view>
        <navigator hover-class="none" url="/pages/todo-detail/todo-detail?id={{todo._id}}">
            <view class="todo-body">

                <view class="todo-title van-ellipsis" style="{{todo.done ? 'color:#a0a0a0;text-decoration:line-through;' :''}}">{{todo.description}}</view>

                <!-- <view class="todo-des van-ellipsis">日期:{{todo.create_date_format}}</view> -->
                <view class="todo-des van-ellipsis">
                    <view wx:if="{{todo.isShowMyday && !todo.fromIndex}}" style="display:flex;aligin-item:center; margin-right:10px;">
                        <view>我的一天</view>
                    </view>
                    <view wx:if="{{todo.isShowDueDate}}" style="color:red; display:flex;aligin-item:center;">
                        <view>{{todo.due_date_format}}</view>
                    </view>
                </view>
            </view>
        </navigator>

        <view class="todo-operate" bindtap="onClickTodoItemRight">
            <van-icon wx:if="{{!todo.isImportant}}" name="star-o" />
            <van-icon wx:else color="{{todo.isImportant ? '#A9C3F8' :''}}" name="star" />
        </view>
    </view>
</view>

wxss

/* components/todo-item/TodoItem.wxss */

.van-ellipsis {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.van-multi-ellipsis--l2 {
    -webkit-line-clamp: 2;
}

.van-multi-ellipsis--l2,
.van-multi-ellipsis--l3 {
    display: -webkit-box;
    overflow: hidden;
    text-overflow: ellipsis;
    -webkit-box-orient: vertical;
}

.van-multi-ellipsis--l3 {
    -webkit-line-clamp: 3;
}

.todo-item-content {
    background-color: #fff;
    border-radius: 15rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 20rpx;
    height: 100rpx;
    /* border-bottom: 1px solid #dedede; */
    margin-bottom: 4px;
}

.todo-body {
    width: 580rpx;
    padding-left: 20rpx;
    font-size: 14px;
    min-height: 30rpx;
}

.todo-body .todo-des {
    font-size: 12px;
    display: flex;
}

.todo-des {
    color: #a0a0a0;
}

js

// components/todo-item/TodoItem.js
Component({
    /**
     * 组件的属性列表
     */
    properties: {
        todo: {
            type: Object,
            value: {}
        }
    },

    /**
     * 组件的初始数据
     */
    data: {
        checked: false
    },

    /**
     * 组件的方法列表
     */
    methods: {
        onChange(event) {
            let done = event.detail
            let todoId = this.data.todo._id
            const db = wx.cloud.database()
            db.collection('todos').doc(todoId).update({
                data: {
                    done: done,
                    complete_date: done ? new Date() : null
                },
                success: function(res) {
                    console.log(res)
                }
            })
            this.setData({
                todo: {
                    ...this.data.todo,
                    done,
                    complete_date: done ? new Date() : null
                }
            });
            this.triggerEvent('checkboxchange', event.detail)
        },
        onClickTodoItem() {
            this.triggerEvent('clicktodoitem')
            wx.navigateTo({
                url: '/pages/todo-detail/todo-detail?'+this.data.todo._id,
            })
        },
        onClickTodoItemRight() {
            let todoId = this.data.todo._id
            const db = wx.cloud.database()
            let isImportant = !this.data.todo.isImportant
            db.collection('todos').doc(todoId).update({
                data: {
                    isImportant
                }
            })
            this.setData({
                todo: {
                    ...this.data.todo,
                    isImportant
                }
            });
            this.triggerEvent('clicktodoright')
        },
        remove(){
            wx.showToast({
                title: 'hi',
            })
        }
    }
})

在这里作者把数据库操作和跳转操作都疯转到了组件中去,这样的好处就是比较省事加上业务高度一致所以可以这样做,不过如果想更加灵活可以把这些与业务耦合的内容放在相关的业务页面去编写会更好,然后再去封装业务代码。

然后再看下todo-input
wxml

<!--components/todo-input/TodoInput.wxml-->
<view class="todo-input-wrapper">
    <view class="add-icon">
        <van-icon name="plus" />
    </view>
    <view class="input-component-wrapper">
        <input bindinput="todoInputHandle" bindconfirm="todoInputConfirmHandle" class="input-component"
            value="{{todoValue}}" placeholder-style="color:#fff;" placeholder="请输入代办事项" />
    </view>
    <view bindtap="todoInputConfirmHandle" class="enter-icon">
        <van-icon name="upgrade" />
    </view>

</view>

wxss

/* components/todo-input/TodoInput.wxss */

.todo-input-wrapper {
    display: flex;
    align-items: center;
    height: 90rpx;
    width: 100%;
    justify-content: space-between;
    background-color: rgba(100, 96, 96,.6);
    color: #fff;
    border-radius: 10rpx;
    font-size: 16px;
}

.add-icon, .enter-icon {
    width: 80rpx;
    display: flex;
    height: 90rpx;
    align-items: center;
    justify-content: center;
}

.input-component-wrapper {
    width: 540rpx;
    padding-left: 20rpx;
}

.input-component {
    width: 100%;
    color: #fff;
    height: 90rpx;
}

js

// components/todo-input/TodoInput.js
import {
    addTodoItem
} from '../../utils/todoDbHelper.js'
import Notify from '../../miniprogram_npm/[@vant](/user/vant)/weapp/notify/notify';

Component({
    /**
     * 组件的属性列表
     */
    properties: {
        pageType: {
            type: Number,
            value: 0
        } // 0我的一天 ,1重要 ,2代办列表
    },

    /**
     * 组件的初始数据
     */
    data: {
        todoValue: ''
    },

    /**
     * 组件的方法列表
     */
    methods: {
        todoInputHandle(e) {
            this.data.todoValue = e.detail.value
        },
        todoInputConfirmHandle(e) {
            let that = this
            let todoValue = this.data.todoValue
            let pageType = this.data.pageType
            console.log('pageType', pageType)
            if (!todoValue) {
                Notify({
                    type: 'warning',
                    message: '请输入代办事项!'
                })
                return
            }
            let addParams = {
                description: todoValue,
            }
            if (pageType === 0) {
                addParams.isMyday = true
                addParams.addMydayDate = new Date()
            }
            if (pageType === 1) {
                addParams.isImportant = true
            }
            console.log('addParams', addParams)

            addTodoItem(addParams).then(res => {
                // res 是一个对象,其中有 _id 字段标记刚创建的记录的 id
                console.log('插入成功', res)
                that.triggerEvent('success', res)
                that.setData({
                    todoValue: ''
                })
            })

        }
    }
})

在这里作者用到了一个操作工具类 todoDbHelper

const db = wx.cloud.database()
const dbCollection = db.collection('todos')

/**
 * 通过id查询单个todo详情
 */
export const queryTodoDetailById = (id) => {
    return dbCollection.where({
        _id: id
    }).get()
}

/**
 * 添加todoItem 三个地方,我的一天,重要,代办列表
 */
export const addTodoItem = (params) => {
    const defaultParams = {
        // description: description, // 描述,标题
        create_date: new Date(), // 创建时间
        isMyday: false,
        addMydayDate: null, // 添加到我的一天的时间
        due_date: null, // 结束时间
        complete_date: null, // 完成时间
        done: false, // 是否完成
        isImportant: false, // 是否重要
        remark: '', // 备注
        type: 0,
        remind: false, // 是否提醒
        remind_date: null // 提醒时间
    }
    return dbCollection.add({
        data: {
            ...defaultParams,
            ...params
        }
    })
}

/**
 * 更新todoItem
 */
export const updateTodoItem = (id, params) => {
    return dbCollection.doc(id).update({
        data: {
            ...params
        }
    })
}

/**
 * 删除todoItem
 */

export const removeTodoItem = (id) => {
    return dbCollection.doc(id).remove()
}

这种封装的方式可以学习,所有数据库操作封装到一个工具类中去执行。但是在这里要说一下,我觉得可以封装的更彻底一点就是所有数据库才做都可以写在这里面,比如:item组件中的update操作。

云开发代码

整体来说,云开发代码比较简单是基础的增删查改,因为业务相对简单,除了上面提到过的在小程序调用的add、update、remove、get之外还有两个云函数。

查询数量:待办数量、重要待办数量

// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init({
    // API 调用都保持和云函数当前所在环境一致
    env: cloud.DYNAMIC_CURRENT_ENV
})

const db = cloud.database()

// 云函数入口函数
exports.main = async(event, context) => {
    const wxContext = cloud.getWXContext()
    // 先取出集合记录总数
    const countResult = await db.collection('todos').where({
        _openid: wxContext.OPENID
    }).count()
    const isImportantResult = await db.collection('todos').where({
        isImportant: true,
        _openid: wxContext.OPENID
    }).count()
    const isImportantCount = isImportantResult.total
    const count = countResult.total

    return {
        count,
        isImportantCount
    }

}

分页查询todo列表和按日期条件查询

// 云函数入口文件
const cloud = require('wx-server-sdk')
const moment = require('moment')

cloud.init({
    // API 调用都保持和云函数当前所在环境一致
    env: cloud.DYNAMIC_CURRENT_ENV
})

const db = cloud.database()
const _ = db.command
const MAX_LIMIT = 100

// 云函数入口函数
exports.main = async(event, context) => {
    const wxContext = cloud.getWXContext()
    // 先取出集合记录总数
    const countResult = await db.collection('todos').count()
    const isImportantResult = await db.collection('todos').where({
        isImportant: true
    }).count()
    const isImportantCount = isImportantResult.total
    const count = countResult.total
    const queryCount = event.count ? event.count : 10
    // 查询参数
    const dbParams = event.dbParams ? event.dbParams : {}
    // openid
    dbParams._openid = wxContext.OPENID
    // 我的一天条件/当天 
    if (dbParams.isMyday) {
        let curDate = moment().format('YYYY-MM-DD');
        let nextDate = moment().add(1, 'days').format('YYYY-MM-DD')
        dbParams.addMydayDate = _.gte(new Date(curDate)).and(_.lte(new Date(nextDate)))
    }
    // 计算需分几次取
    const batchTimes = Math.ceil(queryCount / 100)
    // 
    // 承载所有读操作的 promise 的数组
    const tasks = []
    for (let i = 0; i < batchTimes; i++) {
        const promise = db.collection('todos').where(dbParams).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get()
        tasks.push(promise)
    }
    // 等待所有
    let data = (await Promise.all(tasks)).reduce((acc, cur) => {
        return {
            data: acc.data.concat(cur.data),
            errMsg: acc.errMsg,
        }
    })
    return {
        data: data.data,
        count,
        isImportantCount,
        event
    }

}

总结

整体来说这个小程序很适合新手学习,逻辑相对简单,功能实用性较强。里面有一些代码封装的思维值得学习如:自定义组件、工具类封装这些,如果能封装的更加彻底就更好了。

回到顶部