https://www.ixigua.com/7017702820048536071
https://gitee.com/maoshushare/fast-todolist
本项目是一个fastadmin+微信小程序的实战案例,一个简单的todolist任务管理功能,包括后台数据增删改查、数据统计、api接口开发(小程序对接登录、小程序端管理数据)等。
功能比较简单,覆盖到的知识点不会太多,适合初学者,尤其适合没有做过小程序和fa结合使用的同学。
为便于大家利用碎片时间学习,每节课的时间会尽量控制在10分钟左右。
本项目是模仿滴答清单的部分功能,大概界面是这样:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6vrVqSwq-1641880230864)(https://cdn.jsdelivr.net/gh/978165754/picgo@master/blog/20211011104255.png)]
本项目制作思路:
先引入Validate,use think\Validate;
$rule = [
'openid' => 'require|length:10,30'
];
$msg = [
'openid.require' => '缺少openid',
'openid.length' => 'openid长度不符合',
];
$v = new Validate($rule,$msg);
如果已经存在,则登录,不存在则先注册,然后再自动登录
api相关的拓展教程:https://www.bilibili.com/video/BV1Ji4y1V7ZV?p=11
$post=$this->request->param();
if (!$v->check($post)) {
$this->error('登录失败:' . $v->getError());
}
$u = model('admin/User')->where('openid',$post["openid"])->find();
if($u){
Token::clear($u["id"]);
$this->auth->direct($u["id"]);
$this->success('登录成功', $this->auth->getUserinfo());
}
else{
$username = $post["openid"];
// 初始密码给一个随机数
$password = Random::alnum(15);
$this->auth->register($username,$password,'','',[
"openid"=>$post["openid"]
]);
$this->success('注册成功', $this->auth->getUserinfo());
}
字段:
userid task status tags prio desc
todotime
donetime
giveuptime
createtime updatetime deletetime
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LNJbJLnN-1641880230868)(https://cdn.jsdelivr.net/gh/978165754/picgo@master/blog/20211012141903.png)]
新建一个控制器Xcxtodo,专门写关于任务控制的api
增加和修改写在一起,方便维护
增加
$this->model
->allowField(true)
->save([
"userid"=>$this->uid,
"tags"=>"测试",
"task"=>"测试3331232233",
]);
修改
$this->model
->allowField([
"task"
])
->save([
"task"=>"89898",
],["id"=>20]);
根据任务id删除(软删除)
关于软删除的文档说明:https://www.kancloud.cn/manual/thinkphp5/189658
$this->model->get($id)->delete();
$this->model->destroy($id);
根据关键词查询任务内容(模糊查询)
$task = input("task");
$where=[
"userid"=>$this->uid,
];
if($task){
$where["task"]=["like","%$task%"];
}
$list = $this->model->where($where)->select();
处理标签id为标签文字
$tags = model('admin/Todotags')->where([
"userid"=>$this->uid,
])->column('tags','id');
foreach ($list as $key => &$value) {
$tagsArr = explode(',',$value["tags"]);
$tagsNameArr = [];
foreach ($tagsArr as $id) {
$tagsNameArr[] = $tags[$id] ?? $id;
}
$value["tagsName"] = implode(',',$tagsNameArr);
}
根据关键词查询标签
$tags = input("tags");
$where=[
"userid"=>$this->uid,
];
if($tags){
$where["tags"]=["like","%$tags%"];
}
$list = $this->model->where($where)->group("tags")->column("tags");
改为专表之后:
$tags = input("tags");
$where=[
"userid"=>$this->uid,
];
if($tags){
$where["tags"]=["like","%$tags%"];
}
$list = model('admin/Todotags')->where($where)->select();
根据标签查询任务
$tags = input("tags");
$where=[
"userid"=>$this->uid,
];
if($tags){
$where["tags"]=$tags;
}
$list = $this->model->where($where)->select();
改为专表之后:
注意exp的写法(https://www.kancloud.cn/manual/thinkphp5/135182)
$tags = input("tags");
$where=[
"userid"=>$this->uid,
];
$list = $this->model
->where($where);
if($tags){
$list=$list->where('','exp', "instr(CONCAT( ',', tags, ',' ), ',".$tags.",' )");
}
$list = $list->select();
拓展知识instr:
INSTR(str,substr)
str:从哪个字符串中搜索
substr:要搜索的子字符串
instr()函数不区分大小写
按日期分组:
核心:时间查询( https://www.kancloud.cn/manual/thinkphp5/165789 )
已过期的 (status=1 && todotime 在当前时间之前)
根据自己的项目实际需求来做,可以是当前时间以前,也可以是当前日期以前
$where=[
"userid"=>$this->uid,
"status"=>1,
];
$list =$this->model->where($where)->whereTime('todotime','<',time())->select();
今天的
$list =$this->model->where($where)->whereTime('todotime','d')->select();
明天的
$day = date('Y-m-d',strtotime("+1 day"));
$list =$this->model->where($where)->whereTime('todotime', 'between', [$day.' 00:00:00', $day.' 23:59:59'])->select();
后天到7日内
$day1 = date('Y-m-d',strtotime("+2 day"));
$day2 = date('Y-m-d',strtotime("+7 day"));
$list =$this->model->where($where)->whereTime('todotime', 'between', [$day1.' 00:00:00', $day2.' 23:59:59'])->select();
更久以后
$day2 = date('Y-m-d',strtotime("+7 day"));
$list =$this->model->where($where)->whereTime('todotime', '>', $day2.' 23:59:59')->select();
无计划日期的 (status=1 && todotime为空)
$where=[
"userid"=>$this->uid,
"status"=>1,
];
$list =$this->model->where($where)->whereNull('todotime')->select();
已完成的 (status=2)
$where=[
"userid"=>$this->uid,
"status"=>2,
];
$list =$this->model->where($where)->select();
已放弃的 (status=3)
$where=[
"userid"=>$this->uid,
"status"=>3,
];
$list =$this->model->where($where)->select();
修改和删除限制只能操作当前用户自己的数据
着重分析下update和save的区别(https://www.kancloud.cn/manual/thinkphp5/135189)
update方法:allowField无效,不会更新时间
$res = $this->model
// ->allowField(true) //无效
->where(["id"=>$id,"userid"=> $this->uid])
->update($data);
save方法:isUpdate(true)
if($info = $this->model->get(['id'=>$id,'userid'=>$this->uid])){
$res = $info
->allowField(true)
->isUpdate(true)
->save($data);
}
else{
$this->error($type."失败,未找到数据");
}
$type = "删除".$id."数据";
if($info = $this->model->get(['id'=>$id,'userid'=>$this->uid])){
$res = $info->delete();
}
else{
$this->error($type."失败");
}
if($res){
$this->success($type."成功");
}
else{
$this->error($type."失败");
}
先简单搭建下界面,不需要太完善
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C9MwxdUK-1641880230872)(https://cdn.jsdelivr.net/gh/978165754/picgo@master/blog/20211017152953.png)]
<van-cell icon="todo-list-o" border="{{ false }}">
<view slot="title">
<view class="van-cell-text">收集箱view>
view>
<van-icon slot="right-icon" class="mr-10" name="clock-o"/>
<van-icon slot="right-icon" name="ellipsis" />
van-cell>
<van-cell-group border="{{ false }}">
<van-field
value="{{ sms }}"
center
clearable
label=""
placeholder="+ 添加任务到收集箱,回车即可保存"
border="{{ false }}"
use-button-slot
class="ccc"
>
<view slot="right-icon">
<van-icon name="clock-o" class="mr-10"/>
<van-icon name="ellipsis" />
view>
van-field>
van-cell-group>
<van-collapse value="{{ activeNames }}" bind:change="onChange" border="{{ false }}">
<van-collapse-item name="1" border="{{ false }}">
<view slot="title">已过期<text class="smalltext">4text>view>
<van-cell border="{{ true }}">
<view slot="title">
<van-checkbox value="{{ checked }}" bind:change="onChange">
复选框
van-checkbox>
view>
<view slot="right-icon">
<van-tag round type="success">标签van-tag>
<text class="mr-10 ml-10">10-18text>
<van-icon name="ellipsis" />
view>
van-cell>
<van-cell border="{{ true }}">
<view slot="title">
<van-checkbox value="{{ true }}" bind:change="onChange">
复选框
van-checkbox>
view>
<view slot="right-icon" style="font-size: 28rpx;">
10-18
view>
<van-icon slot="right-icon" name="ellipsis" />
van-cell>
van-collapse-item>
van-collapse>
app.json引入组件
"van-button": "@vant/weapp/button/index",
"van-field": "@vant/weapp/field/index",
"van-cell": "@vant/weapp/cell/index",
"van-cell-group": "@vant/weapp/cell-group/index",
"van-tag": "@vant/weapp/tag/index",
"van-icon": "@vant/weapp/icon/index",
"van-collapse": "@vant/weapp/collapse/index",
"van-collapse-item": "@vant/weapp/collapse-item/index",
"van-checkbox": "@vant/weapp/checkbox/index",
"van-checkbox-group": "@vant/weapp/checkbox-group/index"
小程序开发文档:https://developers.weixin.qq.com/miniprogram/dev/framework/
申请测试号:https://developers.weixin.qq.com/miniprogram/dev/devtools/sandbox.html
1.登录授权,获取openid
wx.login ( https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html )
获取用户信息
let u = wx.getStorageSync('userInfo')
if(u){
console.log(1,u);
this.setData({
userInfo: u,
hasUserInfo:true
})
this.login(u)
}
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
getUserProfile(e) {
// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认
// 开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
wx.getUserProfile({
desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log("用户信息",res);
wx.setStorageSync('userInfo', res.userInfo)
this.login(res.userInfo)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
},
2.根据code获取openid,并且对接登录接口
login(u){
wx.login({
success (res) {
console.log(res);
if (res.code) {
//发起网络请求
wx.request({
url: 'http://todo.tt/api/xcxuser/getopenid',
data: {
code: res.code,
nickName:u.nickName
},
success :(res)=>{
console.log(res);
},
fail:(err)=>{
console.log(err);
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
}
})
接口:
public function getopenid($code)
{
$url = "https://api.weixin.qq.com/sns/jscode2session?appid=$this->AppID&secret=$this->AppSecret&js_code=$code&grant_type=authorization_code";
$res = Http::get($url);
$this->success('成功',['r'=>$this->request->param(),"res"=>$res]);
}
globalData: {
userInfo: null,
token:null,
apidomain:'http://todo.tt/api'
}
request(url,data,callback){
wx.request({
url: gb.apidomain + url,
data: data,
dataType: 'json',
enableCache: true,
enableHttp2: true,
enableQuic: true,
header: {
token:gb.token
},
method: "POST",
// responseType: 'json',
success: (result) => {
callback(result)
},
fail: (res) => {
console.error(res)
},
complete: (res) => {},
})
}
bind:confirm=“confirm_add”
confirm_add(event){
// console.log(event.detail);
let task = event.detail
this.add(task)
},
add(task){
if(task != ''){
this.request('/xcxtodo/addEdit',{
task:task
},(res)=>{
console.log(res);
wx.showToast({
title: res.data.msg,
})
})
}
},
var util = require('../../utils/util')
const formatTime = (time, option) => {
const date = new Date(time)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const week = date.getDay()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
//获取 年月日
if (option == 'YY-MM-DD') return [year, month, day].map(formatNumber).join('-')
//获取 月日
if (option == 'MM-DD') return [month, day].map(formatNumber).join('-')
//获取 年月
if (option == 'YY-MM') return [year, month].map(formatNumber).join('-')
//获取 年
if (option == 'YY') return [year].map(formatNumber).toString()
//获取 月
if (option == 'MM') return [mont].map(formatNumber).toString()
//获取 日
if (option == 'DD') return [day].map(formatNumber).toString()
//获取 年月日 周一 至 周日
if (option == 'YY-MM-DD Week') return [year, month, day].map(formatNumber).join('-') + ' ' + getWeek(week)
//获取 月日 周一 至 周日
if (option == 'MM-DD Week') return [month, day].map(formatNumber).join('-') + ' ' + getWeek(week)
//获取 周一 至 周日
if (option == 'Week') return getWeek(week)
//获取 时分秒
if (option == 'hh-mm-ss') return [hour, minute, second].map(formatNumber).join(':')
//获取 时分
if (option == 'hh-mm') return [hour, minute].map(formatNumber).join(':')
//获取 分秒
if (option == 'mm-dd') return [minute, second].map(formatNumber).join(':')
//获取 时
if (option == 'hh') return [hour].map(formatNumber).toString()
//获取 分
if (option == 'mm') return [minute].map(formatNumber).toString()
//获取 秒
if (option == 'ss') return [second].map(formatNumber).toString()
//默认 时分秒 年月日
return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
const getWeek = n => {
switch(n) {
case 1:
return '星期一'
case 2:
return '星期二'
case 3:
return '星期三'
case 4:
return '星期四'
case 5:
return '星期五'
case 6:
return '星期六'
case 7:
return '星期日'
}
}
module.exports = {
formatTime: formatTime
}
getbyday(opt = {}){
this.request('/xcxtodo/getbyday',opt,(res)=>{
let datalist = res.data.data
for (const key in datalist) {
if (Object.hasOwnProperty.call(datalist, key)) {
const element = datalist[key];
element.forEach((ele,ind) => {
if(ele.tags){
datalist[key][ind].tagsnum = ele.tags.split(',').length
}
if(ele.todotime){
datalist[key][ind].todotime = util.formatTime(ele.todotime*1000,'MM-DD')
}
});
}
}
this.setData({
datalist
})
})
},
groups:{
timeout:"已过期",
today:"今天",
tomorrow:"明天",
seven:"7天",
langtime:"更久以后",
notime:"无日期",
timeend:"已完成",
},
activeNames: ['timeout','today'],
<block wx:for="{{groups}}" wx:if="{{datalist[ind].length>0}}" wx:key="ind" wx:for-item="itm" wx:for-index="ind">
<van-collapse-item name="{{ind}}">
<view slot="title">{{itm}}<text class="smalltext">{{datalist[ind].length}}text>view>
<block wx:for="{{datalist[ind]}}" wx:key="id">
<van-cell>
<view slot="title">
<van-checkbox value="{{ checked }}" bind:change="onChange">{{item.task}}van-checkbox>
view>
<view slot="right-icon">
<van-tag round type="success" wx:if="{{item.tags_number}}">{{item.tags_number}}van-tag>
<text class="mr-10 ml-10" wx:if="{{item.todotime}}">{{item.todotime}}text>
<van-icon name="ellipsis" />
view>
van-cell>
block>
van-collapse-item>
block>
const { data } = res
if(data.code === 1){
this.getbyday()
}
修改的方法:
edit(id,data){
const param={...data,id}
this.request('/xcxtodo/addEdit',param,(res)=>{
// console.log(res);
const { data } = res
if(data.code === 1){
this.getbyday()
}
wx.showToast({
title: data.msg,
})
})
},
getbyday方法优化(格式化完成时间和选中状态)
if(ele.donetime){
datalist[key][k]["donetime"] = util.formatTime(ele.donetime*1000,'MM-DD')
datalist[key][k]["checked"] = true
}
增加选中状态,增加data数据绑定
<van-checkbox value="{{ item.checked }}" data-id="{{item.id}}" data-ind="{{ind}}" data-index="{{index}}" bind:change="onCheckChange">{{item.task}}van-checkbox>
点击checkbox选项时切换状态
onCheckChange(event){
console.log(event);
const {index,ind,id} = event.currentTarget.dataset
const {datalist} = this.data
datalist[ind][index].checked = event.detail
this.edit(id,{
status:event.detail===true?2:1
})
this.setData({
datalist
});
}
1.首先加一个点击右侧按钮弹出编辑项
<van-icon name="ellipsis" bind:click="showEditPopup" data-ind="{{ind}}" data-index="{{index}}" />
2.弹出层放在右侧,里面放几个选项
<van-popup show="{{ showPopup }}" position="right" custom-style="height: 100%" bind:close="onClosePopup">
<van-cell title="删除" icon="delete-o" bind:click="delete"/>
<van-cell title="标签" icon="location-o" />
<van-cell title="改日期" icon="clock-o" />
van-popup>
3.写对应的js方法
showEditPopup(event){
this.setData({
showPopup : true
})
const {index,ind} = event.currentTarget.dataset
const {datalist} = this.data
this.setData({
editItem:datalist[ind][index]
})
},
onClosePopup(){
this.setData({
showPopup : false,
editItem:null
})
},
delete(){
console.log('编辑的是',this.data.editItem);
const {id} = this.data.editItem
this.request('/xcxtodo/delete',{id},(res)=>{
// console.log(res);
const { data } = res
if(data.code === 1){
this.getbyday()
this.onClosePopup()
}
wx.showToast({
title: data.msg,
})
})
}
后续内容将在个人博客上更新:https://maoshu.fun/posts/45372c3c.html