使用APICloud AVM多端框架开发企业移动OA办公的项目实践

简介: 本项目主要是针对企业内部员工使用,除了大部分OA办公常用的功能模块,也有部分定制化的功能模块。后台用的PHP+BootStrap+Easyui。

本项目主要是针对企业内部员工使用,除了大部分OA办公常用的功能模块,也有部分定制化的功能模块。后台用的PHP+BootStrap+Easyui(PS:是不是感觉很久远的技术了)。


功能介绍

1、考勤打卡签到,加班打卡签到

2、办公流程申请、审批

3、通知下发、短信消息提醒

4、个人考勤记录查询,按月统计、钻取查询明细


思维导图

1.png

 

技术要点

Flex布局,amap地图应用,消息推送,短信提醒。


应用模块

2.jpg


项目目录

3.jpg


开发介绍

首页导航

系统首页使用tabLayout,可以将相关参数配置在JSON文件中,再在config.xml中将content的值设置成该JSON文件的路径。如果底部导航没有特殊需求这里强烈建议大家使用tabLayout为APP进行布局,官方已经将各类手机屏幕及不同的分辨率进行了适配,免去了很多关于适配方面的问题。

4.jpg

{
    "name": "root",
    "hideNavigationBar": false,
    "bgColor": "#fff",
    "navigationBar": {
        "background": "#1492ff",
        "shadow": "rgba(0,0,0,0)",
        "color": "#fff",
        "fontSize": 18,
        "hideBackButton": true
    },
    "tabBar": {
        "background": "#fff",
        "shadow": "#eee",
        "color": "#5E5E5E",
        "selectedColor": "#1492ff",
        "textOffset": 3,
        "fontSize": 11,
        "scrollEnabled": true,
        "index": 0,
    "preload": 1,
        "frames": [{
            "name": "home",
            "url": "./pages/index/index.stml",
            "title": "首页"
        }, {
            "name": "notice",
            "url": "./pages/notice/notice.stml",
            "title": "通知"
        }, {
            "name": "records",
            "url": "./pages/records/records.stml",
            "title": "记录"
        }, {
            "name": "user",
            "url": "./pages/wode/wode.stml",
            "title": "我的"
        }],
        "list": [{
            "text": "首页",
            "iconPath": "./images/toolbar/icon-home.png",
            "selectedIconPath": "./images/toolbar/icon-home-selected.png"
        }, {
            "text": "通知",
            "iconPath": "./images/toolbar/icon-notice.png",
            "selectedIconPath": "./images/toolbar/icon-notice-selected.png"
        }, {
            "text": "记录",
            "iconPath": "./images/toolbar/icon-records.png",
            "selectedIconPath": "./images/toolbar/icon-records-selected.png"
        }, {
            "text": "我的",
            "iconPath": "./images/toolbar/icon-user.png",
            "selectedIconPath": "./images/toolbar/icon-user-selected.png"
        }]
    }
}

image.gif

接口调用

将接口调用和接口配置分别封装了2个JS插件,model.js和config.js。这样来统一管理,避免了在每个页面进行接口调用的时候都重复写一遍代码,有效的简化了每个功能页面的代码量,只需要在回调里专注写自己的业务逻辑即可。


插件引用

import {Model} from "../../utils/model.js"
import {Config} from "../../utils/config.js"

image.gif

config.js

class Config{
    constructor(){}
}
Config.restUrl = 'http://127.0.0.1/index.php/Home/Api';
Config.queryrecordsbymonth ='/queryrecordsbymonth';//获取用户本月考勤记录
//省略
export {Config};

image.gif

model.js

import {Config} from './config.js';
class Model {
  constructor() {}
}
/*获取用户本月考勤记录 */
Model.queryrecordsbymonth = function (param, callback){
  param.url = Config.queryrecordsbymonth;
  param.method = 'post';
  this.request(param, callback);
}
/*省略*/
Model.request = function(p, callback) {
  var param = p;
  if (!param.headers) {
      param.headers = {};
  }
  // param.headers['x-apicloud-mcm-key'] = 'SZRtDyzM6SwWCXpZ';
  if (param.data && param.data.body) {
      param.headers['Content-Type'] = 'application/json; charset=utf-8';
  }
  if (param.url) {
      param.url = Config.restUrl + param.url;
  }
  api.ajax(param, function(ret, err) {
      callback && callback(ret, err);
  });
}
export {Model};

image.gif

页面中调用接口

//获取当前用户的本月考勤记录
      recordsbymonth() {
        const params = {
          data:{
            values:{
              userid: api.getPrefs({sync: true,key: 'userid'}),
              secret: Config.secret
            }
          }
        }
        Model.queryrecordsbymonth(params, (res,err) => {
          console.log(JSON.stringify(res));
          console.log(JSON.stringify(err));
          if (res && res.flag == "Success") {
            this.data.dk = res.data.dk;
            this.data.cd = res.data.cd;
            this.data.zt = res.data.zt;
            this.data.tx = res.data.tx;
            this.data.qj = res.data.qj;
          }
          else{
            this.data.dk = 0;
            this.data.cd = 0;
            this.data.zt = 0;
            this.data.tx = 0;
            this.data.qj = 0;
          }
          api.hideProgress();
        });
      },

image.gif

消息推送

消息推动采用了官方的push模块,因为产生消息提醒的事件都是在APP中进行触发,所有就用了官方的push模块;如果存在后台系统操作产生消息提醒的,官方的push模块就不适用了,需要用Jpush等三方消息推送平台模块,配合后台SDK进行消息推送。


用户绑定

//判断是否绑定推送
        if(api.getPrefs({sync: true,key:'pushstatus'})!='02'){
          var push = api.require('push');
          push.bind({
            userName: api.getPrefs({sync: true,key:'name'}),
            userId: api.getPrefs({sync: true,key:'id'})
          }, function(ret, err){
            if( ret ){
              // alert( JSON.stringify( ret) );
              api.toast({
                msg:'推送注册成功!'
              });
              //设置推送绑定状态,启动的时候判断一下
              api.setPrefs({key:'pushstatus',value:'02'});  
            }else{
              // alert( JSON.stringify( err) );
              api.toast({
                msg:'推送注册失败!'
              })
              api.setPrefs({key:'pushstatus',value:'01'});
            }
          });
        }

image.gif

推送消息

//发送抄送通知
      copypush(){
        const params = {
        data:{
            values:{
              secret: Config.secret,
              content:'有一条早晚加班申请已审批完成!'
            }
          }
        }
        Model.createcopytousermessage(params, (res,err) => {
          // console.log(JSON.stringify(res));
          // console.log(JSON.stringify(err));
          if (res && res.flag == "Success") {
            var users = res.data.join(',');
            var now = Date.now();
            var appKey = $sha1.sha1("A61542********" + "UZ" + "6B2246B9-A101-3684-5A34-67546C3545DA" + "UZ" + now) + "." + now;
            api.ajax({
              url : 'https://p.apicloud.com/api/push/message',
              method : "post",
              headers: {
                "X-APICloud-AppId": "A615429********",
                "X-APICloud-AppKey": appKey,
                "Content-Type": "application/json"
              },
              dataType: "json",
              data: {
                "body": {
                  "title": "消息提醒",
                  "content": '有一条早晚加班申请已审批完成!',
                  "type": 2, //– 消息类型,1:消息 2:通知
                  "platform": 0, //0:全部平台,1:ios, 2:android
                  "userIds":users
                }
              }
            }, (ret, err)=> {
              // console.log(JSON.stringify(ret))
              // console.log(JSON.stringify(err))
            });
          }
        }); 
      }

image.gif

Flex布局

flex布局在AVM开发中是重中之重!还是那句话,flex布局写好,有CSS基础,根本就不需要用UI组件,完全可以实现UI的设计稿。


关于flex布局推荐一下阮一峰老师的教程,多读几遍多用,自然就会用的得心应手!上链接:https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html


6.png

通知公告

由于通知公告的内容是在后台通过富文本编辑器编辑的内容,其中会有样式布局的元素,不再是单纯的文字展示,这里使用了AVM中的rich-text组件,这个组件能很好的支持一些html元素标签,能完美的把富文本编辑的内容展现出来。

7.png

<template name='notice_info'>
    <scroll-view class="main" scroll-y>
    <text class="title">{this.data.title}</text>
    <text class="subtitle">{this.data.author}|{this.data.sj}</text>
        <rich-text class="content" nodes={this.data.content}></rich-text>
    </scroll-view>
</template>

image.gif

数据列表及分页查询

数据列表的展示,采用scroll-view标签,通过onrefresherrefresh,onrefresherrefresh出发的事件中进行数据列表的刷新,和分页查询。refresher-triggered这个属性来设置当前下拉刷新状态,true 表示下拉刷新已经被触发,false 表示下拉刷新未被触发。如果想默认下拉刷新一下可以在apiready中将之设置为true,以此来代替执行数据刷新操作。


如果列表中的每一项的元素较少,而且没有样式的特殊要求,也可以使用list-view来实现。


下面是以通知公告列表的完整页面代码。其他页面的列表基本功能都是一致的,只是在每一项的样式及参数个数存在差异。

<template>
  <scroll-view class="main" scroll-y enable-back-to-top refresher-enabled refresher-triggered={refresherTriggered} onrefresherrefresh={this.onrefresherrefresh} onscrolltolower={this.onscrolltolower}>
    <view class="item-box">
      <view class="item" data-id={item.id} onclick={this.openNoticeInfo} v-for="(item, index) in noticeList">
        <text class="item-content">{{item.title}}</text>
        <view class="item-sub">
          <text class="item-info">{{item.dt}}</text>
          <text class="item-info">{{item.author}}</text>
        </view>
      </view>
    </view>
    <view class="footer">
      <text class="loadDesc">{loadStateDesc}</text>
    </view>
  </scroll-view>
</template>
<script>
  import {Model} from '../../utils/model.js'
  import {Config} from "../../utils/config.js"
  import $util from "../../utils/util.js"
  export default {
    name: 'notice',
    data() {
      return{
        noticeList: [],
        skip: 0,
        loading: false,
        refresherTriggered: false,
        haveMoreData: true
      }
    },
    computed: {
      loadStateDesc(){
        if (this.data.loading || this.data.haveMoreData) {
          return '加载中...';
        } else if (this.noticeList.length > 0) {
          return '没有更多啦';
        } else {
          return '暂时没有内容';
        }
      }
    },
    methods: {
      apiready(){
        this.data.refresherTriggered = true;
        this.loadData(false);
      },
      loadData(loadMore) {
        this.data.loading = true;
        var that = this;
        var limit = 20;
        var skip = loadMore?that.data.skip+1:0;
        let params = {
          data:{
            values:{
              secret: Config.secret,
              userid: api.getPrefs({sync: true,key: 'userid'}),
              skip: skip,
              limit: limit
            }
          }
        }
        Model.getNoticeList(params, (res) => {
          if (res && res.flag == 'Success') {
            let notices = res.data;
            that.data.haveMoreData = notices.length == limit;
            if (loadMore) {
              that.data.noticeList = that.data.noticeList.concat(notices);
            } else {
              that.data.noticeList = notices;
            }
            that.data.skip = skip;
          } else {
            that.data.haveMoreData = false;
          }
          that.data.loading = false;
          that.data.refresherTriggered = false;
        });
      },
      //打开通知详情页
      openNoticeInfo: function (e) {
        var id = e.currentTarget.dataset.id;
        $util.openWin({
          name: 'notice_info',
          url: '../notice/notice_info.stml',
          title: '通知详情',
          pageParam:{
            id:id
          }
        });
      },
      /*下拉刷新页面*/
      onrefresherrefresh(){
        this.data.refresherTriggered = true;
        this.loadData(false);
      },
      onscrolltolower() {
        if (this.data.haveMoreData) {
          this.loadData(true);
        }
      }
    }
  }
</script>
<style>
    .main {
        height: 100%;
    background-color: #f0f0f0;
    }
  .item-box{
    background-color: #fff;
    margin: 5px 5px;
  }
  .item{
    border-bottom: 1px solid #efefef;
    margin: 0 10px;
    justify-content:flex-start;
    flex-direction:column;
  }
  .item-content{
    font-size: 17PX;
    margin-top: 10px;
  }
  .item-info{
    font-size: 13PX;
    color: #666;
    margin: 10px 0;
  }
  .item-sub{
    justify-content:space-between;
    flex-direction:row;
  }
  .footer {
        height: 44px;
        justify-content: center;
        align-items: center;
    }
    .loadDesc {
        width: 200px;
        text-align: center;
    }
</style>

image.gif

组件开发

此项目中将模块缺省页和无数据页面封装为组件,方便在有数据查询的页面,不存在数据的情况直接引用组件即可。在事件项目需求中,尽量将通用的代码模块,封装成组件,这样不仅简化了页面代码量,而且很方便维护项目,组件中的内容修改一次,就可以应用到很多的使用组件的页面。


具体的开发教程可参考官方给出的教程并结合官方给出的点餐模板中的教程进行编写。这是官方链接:https://docs.apicloud.com/APICloud/Order-template-description

8.png

需要注意的点是,组件中使用installed,页面中使用apiready,如果组件中使用了apiready不会报错,但是不会执行你想要的结果。

9.png


地图模块使用

本应用中使用的是搞得地图amap,具体使用教程可通过模块使用教程进行详细了解,amp模块包含的功能特别丰富,基本上可以满足99%的关于地图的需求。

10.png

下面主要说明几点在使用高德地图过程中踩过的坑:

1、由于高德地图是原生模块,如果一个页面中地图只是其中一部分的元素的话,就需要注意地图的大小及位置,因为原生模块会遮罩页面元素,所以在固定好地图元素的位置之后,页面中的其他元素也要进行调整,我是用一个空白的view元素来占用地图组件的位置,然后在去调整其他页面的元素。


2、由于本项目中的考勤打卡是根据打卡位置进行了是否外勤的判断,正好用到了isCircleContainsPoint这个方法,但是需要注意的是,此方法只有在调用了open接口之后才有效,因为一开始就是做了一个根据经纬度查找地址信息,用到的getNameFromCoords不需要调用open接口即可。就没有调用open接口,导致后来用isCircleContainsPoint这个接口一直是无效的,都快整郁闷了!

11.png

12.png


3、新版本的高德地图应工信部要求,自本模块1.6.0版本起首次调用本模块前必须先弹出隐私协议,详情参考SDK合规使用方案。之后需先调用 updateMapViewPrivacy,updateSearchPrivacy,否则地图和搜索接口都无效。

如果你的项目之前用的是老版本的amap,后来打包的时候升级成最新的了,一定要加上这个两个接口!

 

var aMap = api.require('aMap');
        aMap.open({
          rect: {
            x: 0,
            y: 80,
            h: api.frameHeight-300
          },
          showUserLocation: true,
          showsAccuracyRing:true,
          zoomLevel: 13,
          center: {
            lon: api.getPrefs({sync: true,key: 'lon'}),
            lat: api.getPrefs({sync: true,key: 'lat'})
          },
          fixedOn: api.frameName,
          fixed: true
        }, (ret, err) => {
          // console.log(JSON.stringify(ret));
          // console.log(JSON.stringify(err));
          if (ret.status) {
            //获取用户位置 并判断是否在范围内500米
            aMap.getLocation((ret, err) => {
              if (ret.status) {
                this.data.lon_now = ret.lon;
                this.data.lat_now = ret.lat;
                //解析当前地理位置
                aMap.getNameFromCoords({
                    lon: ret.lon,
                    lat: ret.lat
                }, (ret, err) => {
                  // console.log(JSON.stringify(ret));
                    if (ret.status) {
                      this.data.address=ret.address;
                      this.data.province = ret.state;
                    } else {
                      api.toast({
                        msg:'解析当前地理位置失败'
                      })
                    }
                });
                aMap.isCircleContainsPoint({
                  point: {
                    lon: api.getPrefs({sync: true,key: 'lon'}),
                    lat: api.getPrefs({sync: true,key: 'lat'})
                  },
                  circle: {
                    center: {           
                      lon: ret.lon,    
                      lat: ret.lat    
                    },
                    radius: this.data.distance
                  }
                }, (ret) => {
                  // console.log(JSON.stringify(ret));
                  if(ret.status){
                    this.data.isout=false;
                    this.data.btn_title='打卡签到';
                  }
                  else{
                    this.data.btn_title='外勤签到';
                    this.data.isout=true;
                    api.toast({
                      msg:'您不在考勤范围内'
                    })
                  }
                });
              } else {
                api.toast({
                  msg:'定位失败,无法签到'
                })
              }
            });
          } else {
            api.toast({
              msg:'加载地图失败'
            })
          }
        });

image.gif

拍照及选择照片

因为项目考勤打卡需要每人每天拍3张照片,而且目前手机的像素较高,导致照片体积过大,严重消耗服务器内存;所以拍照使用的是FNPhotograph模块,自带UI的open接口,可选择拍照照片的质量,可配置使用摄像头方向,同时可配置照片不用存储到相册中,禁用显示相册按钮,保证用户只能现场拍照,可以满足项目需求。

13.png

openCamera (){
        var FNPhotograph= api.require('FNPhotograph');
        FNPhotograph.openCameraView({
            rect: {
              x: 0,
              y: 80,
              w: api.frameWidth,
              h: api.frameHeight-70
            },
            orientation: 'portrait',
            fixedOn: api.frameName,
            useFrontCamera:true,//使用前置摄像头
            fixed: true
        }, (ret) => {
            // console.log(JSON.stringify(ret));
          if(ret.status){
            this.data.istakephoto = true;
          }
        });
      },
      takephoto (){
        var FNPhotograph= api.require('FNPhotograph');
        FNPhotograph.takePhoto({
          quality: 'low',
          qualityValue:30,
          path: 'fs://imagepath',
          album: false
        }, (ret) => {
          // console.log(JSON.stringify(ret));
          this.data.src = ret.imagePath;
          FNPhotograph.closeCameraView((ret) => {
            // console.log(JSON.stringify(ret));
            if (ret.status) {
              this.data.istakephoto = false;
              this.data.isphoto = true;
            }
          });
        });
      },
      showPicture (){
        var photoBrowser = api.require('photoBrowser');
        photoBrowser.open({
            images: [
              this.data.src
            ],
            placeholderImg: 'widget://res/img/apicloud.png',
            bgColor: '#000'
        }, (ret, err) => {
            if (ret) {
              if(ret.eventType=='click'){
                photoBrowser.close();
              }
            } else {
              api.toast({
                msg:'图片预览失败'
              })
            }
        });
      },

image.gif

关于用户头像的设置,用户可选择拍照和从相册中选择照片。同时支持裁剪以满足用户头像设置的需求。裁剪用到的是FNImageClip模块。在使用FNImageClip模块的时候建议新开frame页面,在新的frame页面进行裁剪操作,裁剪完成之后通过推送事件监听来更新头像!

14.png

setavator(){
        api.actionSheet({
          cancelTitle: '取消',
          buttons: ['拍照', '打开相册']
        }, function(ret, err) {
          if (ret.buttonIndex == 3) {
            return false;
          }
          var sourceType = (ret.buttonIndex == 1) ? 'camera' : 'album';
          api.getPicture({
            sourceType: sourceType,
            allowEdit: true,
            quality: 20,
            destinationType:'url',
            targetWidth: 500,
            targetHeight: 500
          }, (ret, err) => {
            if (ret && ret.data) {
              $util.openWin({
                name: 'facemake',
                url: '../wode/facemake.stml',
                title: '头像裁剪',
                pageParam: {
                  faceimg:ret.data
                }
              });
            }
          });
        });
      }

image.gif

<template name='facemake'>
    <view class="page">
    <view class="flowbottom">
      <!-- <button class="btn-out" tapmode onclick="closeclip">取消</button>
      <button class="btn" tapmode onclick="saveclip">确定</button>
      <button class="btn-off" tapmode onclick="resetclip">重置</button> -->
      <text class="btn-out" tapmode onclick="closeclip">取消</text>
      <text class="btn" tapmode onclick="saveclip">确定</text>
      <text class="btn-off" tapmode onclick="resetclip">重置</text>
    </view>
    </view>
</template>
<script>
  import {Model} from "../../utils/model.js"
  import {Config} from "../../utils/config.js"
  export default {
    name: 'facemake',
    data() {
      return{
        facepic:'',
        src:''
      }
    },
    methods: {
      apiready(){//like created
        //取得图片地址
        this.data.facepic=api.pageParam.faceimg;
        FNImageClip = api.require('FNImageClip');
        FNImageClip.open({
          rect: {
            x: 0,
            y: 0,
            w: api.winWidth,
            h: api.winHeight-75
          },
          srcPath: this.data.facepic,
          style: {
            mask: '#999',
            clip: {
              w: 200,
              h: 200,
              x: (api.frameWidth-200)/2,
              y: (api.frameHeight-275)/2,
              borderColor: '#fff',
              borderWidth: 1,
              appearance: 'rectangle'
            }
          },
          fixedOn: api.frameName
        }, (ret, err) =>{
          // console.log(JSON.stringify(ret));
          // console.log(JSON.stringify(err));
        });
      },
      closeclip(){
        FNImageClip = api.require('FNImageClip');
        FNImageClip.close();
        api.closeWin();
      },
      saveclip(){
        FNImageClip = api.require('FNImageClip');
        FNImageClip.save({
          destPath: 'fs://imageClip/result.png',
          copyToAlbum: true,
          quality: 1
        },(ret, err)=>{
          // console.log(JSON.stringify(ret));
          // console.log(JSON.stringify(err));
          this.data.src = ret.destPath;
          if(ret) {
            api.showProgress();
            const params = {
              data:{
                values:{
                  userid: api.getPrefs({sync: true,key: 'userid'}),
                  secret: Config.secret
                },
                files: {'file':[this.data.src]}
              }
            }
            Model.updateuseravator(params, (res,err) => {
              // console.log(JSON.stringify(res));
              // console.log(JSON.stringify(err));
              if (res && res.flag == "Success") {
                //广播完善头像事件
                api.sendEvent({
                  name: 'setavator',
                  extra: {
                    key: res.data
                  }
                });
                api.setPrefs({key:'avator',value:res.data});
                api.closeWin();
              }
              else{
                api.toast({
                  msg:'网络错误,请稍后重试!'
                })
              }
              api.hideProgress();
            });
          } else{
            api.toast({
              msg:'网络错误,请稍后重试!'
            })
          }
        });
      },
      resetclip(){
        FNImageClip = api.require('FNImageClip');
        FNImageClip.reset();
      }
    }
  }
</script>
<style>
    .page {
        display: flex;
    flex-flow: row nowrap;
    height: 100%;
    width: 100%;
    }
  .flowbottom{
    width: 100%;
    align-self: flex-end;
    padding: 10px;
    flex-flow: row nowrap;
    justify-content: space-around;
  }
  .btn {
    display: block;
    height: 30px;
    background:#1492ff;
    border-radius: 5px;
    color: #fff;
    font-size: 16px;
    padding: 5px 20px;
  }
  .btn-out {
    display: block;
    height: 30px;
    background:#666;
    border-radius: 5px;
    color: #fff;
    font-size: 16px;
    padding: 5px 20px;
  }
  .btn-off {
    display: block;
    height: 30px;
    background:#ec7d15;
    border-radius: 5px;
    color: #fff;
    font-size: 16px;
    padding: 5px 20px;
  }
</style>

image.gif

图片预览

项目中很多页面涉及到图片预览的功能,分为单图预览和多图预览。图片预览采用的是photoBrowser 模块。


photoBrowser 是一个图片浏览器,支持单张、多张图片查看的功能,可放大缩小图片,支持本地和网络图片资源。若是网络图片资源则会被缓存到本地,缓存到本地上的资源可以通过 clearCache 接口手动清除。同时本模块支持横竖屏显示,在本app支持横竖屏的情况下,本模块底层会自动监听当前设备的位置状态,自动适配横竖屏以展示图片。使用此模块开发者看实现炫酷的图片浏览器。

15.png

<view class="item-bottom" v-if="item.accessory">
    <view v-for="p in item.accessory.split(',')"  data-url={item.accessory} @click="showPicture">
  <image class="item-bottom-pic" :src="this.data.fileaddr+p" mode="aspectFill"></image>
  </view>       
</view>

image.gif

//查看大图
      showPicture(e){
        let url = e.currentTarget.dataset.url;
        var urlarr= url.split(',');
        var images=[];
        urlarr.forEach(item => {
          images.push(this.data.fileaddr+item);
        });
        // console.log(JSON.stringify(images));
        var photoBrowser = api.require('photoBrowser');
        photoBrowser.open({
          images: images,
          bgColor: '#000'
        }, function(ret, err) {
          if(ret.eventType=='click'){
            photoBrowser.close();
          }
        });
      }

image.gif

清除缓存

由于项目中有很多拍照,查看照片,在使用的过程中,就会产生很多的缓存,缓存多了会导致应用反应变慢。所以在应用中增加了清楚缓存的功能,用的是官方提供的api.clearCache。


在个人中心 apiready中先获取到应用中的缓存,然后点击清除缓存按钮即可清除。

<view class="card_title" onclick="clearCache">
  <image class="card_icon" src="../../images/icon/W_17.png" mode="scaleToFill"></image>
  <text class="card_item">缓存</text>
  <text class="card_right_1">{cache}M</text>
</view>

image.gif

clearCache(){
  api.clearCache(() => {
      api.toast({
        msg: '清除完成'
    });
  });
    this.data.cache=0;
},

image.gif

注册页面、发送手机验证码

核心代码在 如何在发送验证码成功之后,设置再次发动验证码倒计时读秒及禁用点击事件。

16.png

17.png

<template name='register'>
    <view class="page">
      <view class="blank">
      <image class="header" src="../../images/back/b_01.png" mode="scaleToFill"></image>
    </view>
    <view class="item-box">
      <input class="item-input" placeholder="请输入11位手机号码" keyboard-type="tel" oninput="getPhone"/>
    </view>
     <view class="verification-code">
      <input class="code-input" placeholder="输入验证码" keyboard-type="number" oninput="getCode"/>
      <text v-show={this.data.show} class="code-btn" @click={this.sendCode}>获取验证码</text>
      <text v-show={!this.data.show} class="code-btn">{this.data.count}s</text>
     </view>
     <view class="item-box">
      <input class="item-input" placeholder="输入密码(6-20位字符)" type="password" oninput="getPassword"/>
     </view>
     <view class="item-box">
      <input class="item-input" placeholder="确认密码(6-20位字符)" type="password" oninput="getPasswordAgain"/>
     </view>
     <view class="item-box">    
      <button class="btn" tapmode onclick="toresigter">注册</button>
    </view>
    </view>
</template>
<script>
  import {Model} from "../../utils/model.js"
  import {Config} from "../../utils/config.js"
  import $util from "../../utils/util.js"
  export default {
    name: 'register',
    data() {
      return{
        show:true,
        count: '',
          timer: null,
        phone:'',
        code:'',
        password:'',
        passwordagain:''
      }
    },
    methods: {
      apiready(){//like created
      },
      getPhone(e){
        this.data.phone=e.detail.value;
      },
      getCode(e){
        this.data.code=e.detail.value;
      },  
      getPassword(e){
        this.data.password=e.detail.value;
      },
      getPasswordAgain(e){
        this.data.passwordagain=e.detail.value;
      },
      sendCode(){
        if(this.data.phone==''||this.data.phone.length !=11){
          api.toast({
            msg:'请填写正确的手机号!'
          })
          return false;
        }
        const TIME_COUNT = 120;
        if (!this.timer) {
          this.count = TIME_COUNT;
          this.show = false;
          this.timer = setInterval(() => {
          if (this.count > 0 && this.count <= TIME_COUNT) {
              this.count--;
            } else {
              this.show = true;
              clearInterval(this.timer);
              this.timer = null;
            }
          }, 1000)
        }
        //后台发送验证码
        api.showProgress();
        const params = {
          data:{
            values:{
              phone: this.data.phone,
              secret: Config.secret
            }
          }
        }
        Model.sendphonecode(params, (res,err) => {
          // console.log(JSON.stringify(res));
          // console.log(JSON.stringify(err));
          if (res && res.flag == "Success") {
            api.toast({
              msg:'已发送,请注意查收'
            })
          }
          else{
            api.toast({
              msg:res.msg
            });
          }
          api.hideProgress();
        });
      },
      toresigter(){
        if(this.data.phone=='' || this.data.phone.length !=11){
          api.toast({
            msg:'请填写正确的11位手机号!'
          })
          return false;
        }
        if(this.data.code==''){
          api.toast({
            msg:'请填写验证码!'
          })
          return false;
        }
        if(this.data.password==''){
          api.toast({
            msg:'请填写新密码!'
          })
          return false;
        }
        else{
          if(this.data.passwordagain==''){
            api.toast({
              msg:'请填写确认密码!'
            })
            return false;
          }
          else if(this.data.passwordagain != this.data.password){
            api.toast({
              msg:'密码不一致!'
            })
            return false;
          }
        }
        api.showProgress();
        const params = {
          data:{
            values:{
              secret: Config.secret,
              phone:this.data.phone,
              pwd:this.data.password,
              code:this.data.code
            }
          }
        }
        Model.resigeruser(params, (res,err) => {
          // console.log(JSON.stringify(res));
          // console.log(JSON.stringify(err));
          if (res && res.flag == "Success") {
            api.alert({
              title: '提醒',
              msg: '注册成功,即将跳转登陆',
            }, function(ret, err) {
              api.closeWin();
            });
          }
          else{
            api.toast({
              msg:res.msg
            });
          }
          api.hideProgress();
        });
      }
    }
  }
</script>
<style>
    .page {
        height: 100%;
    width: 100%;
    flex-flow: column;
    justify-content: flex-start;
    }
  .blank{
    height: 300px;
    margin-bottom: 50px;
  }
  .header{
    height: 300px;
    width: 100%;
  }
  .item-box{
    margin: 10px 20px;
    border-bottom: 1px solid #f0f0f0;
  }
  .item-input{
    height: 40px;
    width: 100%;
    border-radius: 5px;
    border: none;
  }
  .verification-code{
    flex-flow: row;
    margin: 10px 20px;
    justify-content: space-between;
    border-bottom: 1px solid #f0f0f0;
  }
  .code-input{
    height: 40px;
    width: 70%;
    border-radius: 5px;
    border: none;
  }
  .code-btn{
    height: 40px;
    color: #1492ff;
  }
  .btn{
    display: block;
    width: 100%;
    height: 50px;
    background:#1492ff;
    border-radius: 5px;
    color: #fff;
    font-size: 20px;
    font-weight: bolder;
    padding: 0;
    margin-top: 10px;
  }
</style>

image.gif

后台系统

登陆接口、注册接口、发送手机验证码、列表查询接口,其中手机短信用的是阿里的短信。


阿里短信的SDK通过 composer安装,在需要调用的php文件中头部引用即可。

<?php
namespace Home\Controller;
require 'vendor/autoload.php';    // 注意位置一定要在 引入ThinkPHP入口文件 之前
use Think\Controller;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
class ApiController extends Controller {
    //用户登录
    public function login(){
      checkscret('secret');//验证授权码
      checkdataPost('phone');//手机号
      checkdataPost('password');//密码
      $map['phone']=$_POST['phone'];
      $map['password']=$_POST['password'];
      $map['ischeck']='T';
      $releaseInfo=M()->table('user')
      ->field('id,name,phone,role,part as partid,user_num as usernum,usercenter,avator')->where($map)->find();
      if($releaseInfo){
          returnApiSuccess('登录成功',$releaseInfo);
        }
        else{
          returnApiError( '登录失败,请稍后再试');
          exit();
        }
    }
    //用户注册
    public function resigeruser(){
      checkscret('secret');//验证授权码
      checkdataPost('phone');//手机号
      checkdataPost('password');//密码
      checkdataPost('code');//验证码
      $phone=$_POST['phone'];
      $password=$_POST['password'];
      $code=$_POST['code'];
      //后台再次验证手机号码有效性
      $ckphone=checkphone($phone);
      if($ckphone=='T'){
        $code_s=S($phone);
        if($code_s==$code_s_s){
          $data['phone']=$phone;
          $data['password']=$password;
          $data['role']='01';//注册用户
          $data['resiger_time']=time();
          $releaseInfo=M()->table('user')->data($data)->add();
          if($releaseInfo){
            //注销session
            S($phone,'');
            returnApiSuccess('注册成功',$releaseInfo);
          }
          else{
            returnApiError( '注册失败,请稍后再试');
            exit();
          }
        }
        else{
          returnApiError('验证码已失效,请重新获取');
          exit();
        }
      }
      else{
        returnApiError('手机号已注册!');
        exit();
      }
    }
    //手机发送验证码
    public function sendphonecode(){
      checkscret('secret');//验证授权码
      checkdataPost('phone');//手机号
      $phone=trim($_POST['phone']);
      $ckphone=checkphone($phone);
      if($ckphone=='T'){//尚未注册手机号
        //生成6位验证码
        $code = substr(base_convert(md5(uniqid(md5(microtime(true)),true)), 16, 10), 0, 6);
        //发送验证码
        AlibabaCloud::accessKeyClient(C('accessKeyId'), C('accessSecret'))
                        ->regionId('cn-beijing')
                        ->asDefaultClient();
        try {
            $param = array("code"=>$code);
            $result = AlibabaCloud::rpc()
                      ->product('Dysmsapi')
                      // ->scheme('https') // https | http
                      ->version('2022-01-25')
                      ->action('SendSms')
                      ->method('POST')
                      ->host('dysmsapi.aliyuncs.com')
                      ->options([
                            'query' => [
                            'RegionId' => "cn-beijing",
                            'PhoneNumbers' => $phone,
                            'SignName' => "*******有限公司",
                            'TemplateCode' => "SMS_*******",
                            'TemplateParam' => json_encode($param),
                          ],
                      ])
                      ->request();
           if($result['Code'] == 'OK'){
              S($phone,$code,120);//设置一个120秒的过期时间
              returnApiSuccess('发送成功',$result);
            }
            else{
              returnApiError( '发送失败,请稍后再试');
              exit();
            }
        } catch (ClientException $e) {
            returnApiError( '发送失败,请稍后再试');
            exit();
        }
      }
      else{
          returnApiError('手机号已注册!');
          exit();
      }
    }
    //查询用户加班记录
    public function queryovertime(){
      checkscret('secret');//验证授权码
      checkdataPost('userid');//ID
      checkdataPost('limit');//下一次加载多少条
      $userid=$_POST['userid'];
      //分页需要的参数
      $limit=$_POST['limit'];
      $skip=$_POST['skip'];
      if(empty($skip)){
        $skip=0;
      }
      //查询条件
      $map['userid']=$userid;
      $releaseInfo=M()->table('overtime_records')->field('id,kssj,ksrq,jsrq,ksbz,jsbz,jssj,kswz,jswz,kszp,jszp,zgsp,jlsp,xzsp,zgsp_time,jlsp_time')->where($map)->limit($limit*$skip,$limit)->order('kssj desc')->select();
      if($releaseInfo){
        returnApiSuccess('查询成功',$releaseInfo);
      }
      else{
        returnApiSuccess('查询成功',[]);
        exit();
      }  
    }
}

image.gif

后台系统页面关于easyui和bootstrap的引用

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>示例</title>
    <!-- jquery - boot -库文件 -->
    <script src="__PUBLIC__/script/jquery.1.11.1.js"></script>
    <script src="__PUBLIC__/script/bootstrap.min.js"></script>
    <!-- Bootstrap -->
    <link href="__PUBLIC__/css/bootstrap.min.css" rel="stylesheet">
    <!-- Bootstrap -->
    <!--easyui包含文件-->
    <link rel="stylesheet" type="text/css" href="__PUBLIC__/plugins/easyui1.5.3/themes/material/easyui.css">
    <link rel="stylesheet" type="text/css" href="__PUBLIC__/plugins/easyui1.5.3/themes/icon.css">
    <script type="text/javascript" src="__PUBLIC__/plugins/easyui1.5.3/jquery.easyui.min.js"></script>
    <script type="text/javascript" src="__PUBLIC__/plugins/easyui1.5.3/locale/easyui-lang-zh_CN.js"></script>
    <!-- end easyui -->
    <!--layer-->
    <script type="text/javascript" src="__PUBLIC__/plugins/layer/layer.js"></script>
    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="__PUBLIC__/script/html5shiv.js"></script>
    <script src="__PUBLIC__/script/respond.js"></script>
    <![endif]-->
</head>

image.gif

主要用到了bootstrap的栅格布局,作为页面布局的使用。

18.png

eaysui用的是1.5.3版本,用到了下图中的这些控件。具体使用说明可以下载一个chm API使用手册。

19.png

20.png


html页面

<div class="container-fluid">
        <div class="row">
            <div class="col-md-12 mainbox" id="mainbox">
                <!--menubegin-->
                <div class="datamenubox" id="leftmenu">
                    <div class="menuhead">****</div>
                    <!-- treein -->
                    <div class="treein" id="menuin">
                        <ul class="list-group smenu">
                            <volist name="menulist" id="vo">
                                <a href="{:U($vo[url])}"><li class="list-group-item" id="{$vo.url}"><i class="fa fa-angle-right"></i>{$vo.name}</li></a>
                            </volist>
                        </ul>
                    </div>
                </div>
                <!--menuend-->
                <!--mainboxbegin-->
                <div class="col-md-12 rights" id="right">
                    <!-- 筛选 -->
                    <div class="searchitem">
                        <div class="row">
                            <div class="col-md-12">
                                <input class="easyui-combobox" name="q_user" id="q_user" style="width:200px" data-options="label:'登记人:',valueField:'id',textField:'text',panelHeight:'180'">                       
                                <input class="easyui-textbox" name="q_cphm" id="q_cphm" style="width:200px" data-options="label:'车牌号码:'">    
                                <input class="easyui-datebox" name="q_ksrq" id="q_ksrq" style="width:200px" data-options="label:'开始日期:'">
                                <input class="easyui-datebox" name="q_jsrq" id="q_jsrq" style="width:200px" data-options="label:'结束日期:'">                                                                 
                            </div>
                        </div>
                        <div class="blank10"></div>
                        <div class="row">
                            <div class="col-md-12">
                                <div class="btnin" id="normal">
                                    <button class="btn btn-danger" id="querybtn">查询</button>
                                    <button class="btn btn-success" id="exportbtn">导出Excel</button>
                                    <button class="btn btn-info" id="delbtn">删除</button>
                                </div>
                                <div class="btnin" id="super">
                                    <button class="btn btn-danger" id="querybtn">查询</button>
                                    <button class="btn btn-success" id="exportbtn">导出Excel</button>
                                    <button class="btn btn-info" id="delbtn">删除</button>
                                    <button class="btn btn-info" id="checkbtn">审核</button>
                                </div>
                            </div>
                        </div>
                        <!-- end 筛选 -->
                    </div>
                    <!-- listtable -->
                    <div>
                        <!-- gridview row -->
                        <table id="dg"></table>
                        <!-- end gridview row -->
                    </div>
                    <!--mainboxend-->
                </div>
            </div>
        </div>
        <!-- indexmain end -->
    </div>

image.gif

js部分

<script>
        $(document).ready(function() {
            //初始化页面
            loaddg();
            //用户列表
            LoadDDL('q_user','USER');
        });
        //加载数据列表
        function loaddg() {
            $('#dg').datagrid({
                loadMsg: '正在查询,请稍后...',
                title: '',
                height: $(window).height() - 300,
                url: '{:U(\'queryvehiclefixed\')}',
                queryParams: {
                    user: $('#q_user').combobox('getValue'),
                    cphm: $('#q_cphm').textbox('getValue'),
                    ksrq: $('#q_ksrq').datebox('getValue'),
                    jsrq: $('#q_jsrq').datebox('getValue')
                },
                nowrap: false,
                striped: true,
                collapsible: false,
                loadMsg: '正在加载,请稍后。。。',
                remoteSort: false,
                singleSelect: true,
                pageSize: 100,
                idField: 'id',
                pagination: true,
                rownumbers: true,
                pagination: true,
                pageNumber: 1,
                pageSize: 20,
                pageList: [20, 40, 80, 160],
                fitColumns: true,
                columns: [
                    [{
                        field: 'cphm',
                        title: '车牌号码',
                        width: 50
                    }, {
                        field: 'date',
                        title: '申请时间',
                        width: 70
                    }, {
                        field: 'user',
                        title: '申请人',
                        width: 70
                    }, {
                        field: 'part',
                        title: '所属部门',
                        width: 70
                    }, {
                        field: 'description',
                        title: '问题描述',
                        width: 100
                    }, {
                        field: 'mileage',
                        title: '公里数',
                        width: 50
                    }, {
                        field: 'zgsp',
                        title: '主管审批',
                        width: 50,
                        styler: function(value,row,index){              
                            if (value =='同意'){                    
                                return 'color:green;';
                            }
                            else if(value == '拒绝'){
                                return 'color:red;';
                            }               
                        }          
                    }]
                ]             
            });
            $("#querybtn").click(function() {
                $('#dg').datagrid('load', {
                    "user": $('#q_user').combobox('getValue'),
                    "cphm": $('#q_cphm').textbox('getValue'),
                    "ksrq": $('#q_ksrq').datebox('getValue'),
                    "jsrq": $('#q_jsrq').datebox('getValue')
                });
            });
        }
    //删除
    $('#delbtn').click(function(){
        var row = $('#dg').datagrid('getSelected');
        if(row){
            layer.confirm('您确定要删除选中的数据?', {
                btn: ['是','否'] //按钮
                }, function(){
                    var option = {
                        type: "POST",
                        url: "{:U('delvehiclefixed')}",
                        data: {id:row.id},
                        success: function (data) {
                            layer.closeAll();
                            layer.msg(data);
                            $('#dg').datagrid('reload');
                        }
                    };
                    $.ajax(option);
                }, function(){
                    layer.closeAll();
                });
            }
        else{
            layer.msg('请选择需要删除的数据!');
        }
    })
    //审核
     $('#checkbtn').click(function(){
        var row = $('#dg').datagrid('getSelected');
        if(row){
            layer.confirm('请对此条申请做出审核', {
                btn: ['同意','不同意'] //按钮
                }, function(){
                    var option = {
                        type: "POST",
                        url: "{:U('checkvehiclefixed')}",
                        data: {id:row.id,ret:'02'},
                        success: function (data) {
                            layer.closeAll();
                            layer.msg(data);
                            $('#dg').datagrid('reload');
                        }
                    };
                    $.ajax(option);
                }, function(){
                    var option = {
                        type: "POST",
                        url: "{:U('checkvehiclefixed')}",
                        data: {id:row.id,ret:'03'},
                        success: function (data) {
                            layer.closeAll();
                            layer.msg(data);
                            $('#dg').datagrid('reload');
                        }
                    };
                    $.ajax(option);
                });
            }
        else{
            layer.msg('请选择需要审核的数据!');
        }
    })
    </script>

image.gif

目录
相关文章
|
9天前
|
安全 搜索推荐 数据安全/隐私保护
点晴免费OA办公系统:高效协同,安全易用
信息技术发展推动企业信息化,即企业利用现代技术提升生产、经营、管理效率,增强竞争力。点晴免费OA系统作为信息化管理的基础,是实现企业信息化的关键手段。
22 2
|
1月前
|
数据安全/隐私保护
点晴OA办公系统让企业变得高效协同
随着企业信息化进程的加快,很多企业开始寻求使用企业管理免费OA办公系统来提高工作效率。然而,有些些企业可能缺乏信息化经验,对技术一无所知,甚至从未接触过OA办公系统。在这种情况下,企业需要寻求功能比较全面的OA办公系统,以满足企业的实际需求。
41 1
|
20天前
|
安全 数据可视化 搜索推荐
点晴免费OA:赋能企业高效管理,驱动数字化转型
在当今信息化、数字化快速发展的时代,企业对于高效、智能、安全的办公管理系统需求日益迫切。点晴免费OA系统,是真正完全免费OA办公系统,凭借其卓越的性能,丰富的功能,成为越来越多企业数字化转型的青睐。
40 0
|
30天前
|
敏捷开发 数据可视化 数据挖掘
哪些OA任务管理系统值得推荐?4款高效办公工具介绍
在现代企业中,OA(办公自动化)任务管理系统是提升工作效率和团队协作的关键工具。本文介绍了4款备受推崇的OA任务管理系统:板栗看板、Trello、Asana和Monday.com,分别从提高工作效率、增强团队协作、优化资源分配和提升工作质量等方面进行了详细说明,为用户提供全面的参考和选择指南。
|
2月前
|
数据安全/隐私保护 UED
免费OA办公系统的实力派:点晴OA
点晴OA办公系统是一款面向中小企业的办公自动化解决方案,旨在提高工作效率和优化管理流程。它通过提供多维度的功能模块结构、高度的定制化能力、友好的用户界面以及安全可靠的数据保护机制,满足企业日常办公的多样化需求。以下是关于点晴OA办公系统的详细介绍:
88 0
|
1月前
|
存储 安全 数据安全/隐私保护
如何明智选择免费OA系统的关键因素
在数字化办公日益普及的今天,选择一款合适的免费OA系统对于企业提升工作效率和管理水平至关重要。不管是办公的便捷方便,还是与其他平台的融合,免费OA系统的选择,需要看这几点,易用性、开放性、稳定性、服务性、实用性、安全性。
22 0
|
2月前
|
搜索推荐 BI 数据处理
点晴OA系统让考勤管理不再头疼
在当今数字化管理趋势下,点晴OA办公系统中的考勤管理作为企业内部管理的重要组成部分,其自动化和智能化水平的提高在提高企业运营效率和员工满意度方面发挥着重要作用。
53 4
|
3月前
|
Java uml
某OA系统需要提供一个假条审批的模块,如果员工请假天数小于3天,主任可以审批该请假条;如果员工请假天数大于等于3天,小于10天,经理可以审批;如果员工请假天数大于等于10天,小于30天,总经理可以审批
该博客文章通过一个OA系统中的请假审批模块示例,使用Java语言实现了职责链模式,展示了如何根据不同的请假天数由不同级别的领导进行审批,并讨论了职责链模式的优缺点。
某OA系统需要提供一个假条审批的模块,如果员工请假天数小于3天,主任可以审批该请假条;如果员工请假天数大于等于3天,小于10天,经理可以审批;如果员工请假天数大于等于10天,小于30天,总经理可以审批
|
3月前
|
JavaScript 前端开发 搜索推荐
【Vue 2】一个功能强大OA办公系统,开源且免费!!
【Vue 2】一个功能强大OA办公系统,开源且免费!!
|
4月前
|
监控 BI
点晴免费OA办公系统全面了解看这里
在当今数码化办公环境中。免费OA办公系统已经成为企业管理的核心工具之一。免费OA办公平台是一种集成了各种办公工具、软件和流程的综合系统,旨在提高工作效率、降低沟通成本,并推动组织数字化转型。
65 4