使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

海的盡頭 發表於 2022-05-16
框架

本專案主要是針對企業內部員工使用,除了大部分OA辦公常用的功能模組,也有部分定製化的功能模組。後臺用的PHP+BootStrap+Easyui(PS:是不是感覺很久遠的技術了)。

功能介紹

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

2、辦公流程申請、審批

3、通知下發、簡訊訊息提醒

4、個人考勤記錄查詢,按月統計、鑽取查詢明細

思維導圖

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

技術要點

Flex佈局,amap地圖應用,訊息推送,簡訊提醒。

應用模組

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

專案目錄

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

開發介紹

首頁導航

系統首頁使用tabLayout,可以將相關引數配置在JSON檔案中,再在config.xml中將content的值設定成該JSON檔案的路徑。如果底部導航沒有特殊需求這裡強烈建議大家使用tabLayout為APP進行佈局,官方已經將各類手機螢幕及不同的解析度進行了適配,免去了很多關於適配方面的問題。
使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

{
    "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"
        }]
    }
}

介面呼叫

將介面呼叫和介面配置分別封裝了2個JS外掛,model.js和config.js。這樣來統一管理,避免了在每個頁面進行介面呼叫的時候都重複寫一遍程式碼,有效的簡化了每個功能頁面的程式碼量,只需要在回撥裡專注寫自己的業務邏輯即可。

外掛引用

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

config.js

class Config{
    constructor(){}
}
Config.restUrl = 'http://127.0.0.1/index.php/Home/Api';
 
Config.queryrecordsbymonth ='/queryrecordsbymonth';//獲取使用者本月考勤記錄
//省略
export {Config}; 

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};

頁面中呼叫介面

            //獲取當前使用者的本月考勤記錄
            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();
                });
            },

訊息推送

訊息推動採用了官方的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'});
                        }
                    });
                }

推送訊息

//傳送抄送通知
            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))
                        });
                    }
                });    
            }

Flex佈局

flex佈局在AVM開發中是重中之重!還是那句話,flex佈局寫好,有CSS基礎,根本就不需要用UI元件,完全可以實現UI的設計稿。

關於flex佈局推薦一下阮一峰老師的教程,多讀幾遍多用,自然就會用的得心應手!上鍊接:https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

通知公告

由於通知公告的內容是在後臺通過富文字編輯器編輯的內容,其中會有樣式佈局的元素,不再是單純的文字展示,這裡使用了AVM中的rich-text元件,這個元件能很好的支援一些html元素標籤,能完美的把富文字編輯的內容展現出來。

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

<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>

資料列表及分頁查詢

資料列表的展示,採用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>

元件開發

此專案中將模組預設頁和無資料頁面封裝為元件,方便在有資料查詢的頁面,不存在資料的情況直接引用元件即可。在事件專案需求中,儘量將通用的程式碼模組,封裝成元件,這樣不僅簡化了頁面程式碼量,而且很方便維護專案,元件中的內容修改一次,就可以應用到很多的使用元件的頁面。

具體的開發教程可參考官方給出的教程並結合官方給出的點餐模板中的教程進行編寫。這是官方連結:https://docs.apicloud.com/APICloud/Order-template-description

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

需要注意的點是,元件中使用installed,頁面中使用apiready,如果元件中使用了apiready不會報錯,但是不會執行你想要的結果。

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

地圖模組使用

本應用中使用的是搞得地圖amap,具體使用教程可通過模組使用教程進行詳細瞭解,amp模組包含的功能特別豐富,基本上可以滿足99%的關於地圖的需求。

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

下面主要說明幾點在使用高德地圖過程中踩過的坑:

1、由於高德地圖是原生模組,如果一個頁面中地圖只是其中一部分的元素的話,就需要注意地圖的大小及位置,因為原生模組會遮罩頁面元素,所以在固定好地圖元素的位置之後,頁面中的其他元素也要進行調整,我是用一個空白的view元素來佔用地圖元件的位置,然後在去調整其他頁面的元素。

2、由於本專案中的考勤打卡是根據打卡位置進行了是否外勤的判斷,正好用到了isCircleContainsPoint這個方法,但是需要注意的是,此方法只有在呼叫了open介面之後才有效,因為一開始就是做了一個根據經緯度查詢地址資訊,用到的getNameFromCoords不需要呼叫open介面即可。就沒有呼叫open介面,導致後來用isCircleContainsPoint這個介面一直是無效的,都快整鬱悶了!
使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

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:'載入地圖失敗'
                        })
                    }
                });

拍照及選擇照片

因為專案考勤打卡需要每人每天拍3張照片,而且目前手機的畫素較高,導致照片體積過大,嚴重消耗伺服器記憶體;所以拍照使用的是FNPhotograph模組,自帶UI的open介面,可選擇拍照照片的質量,可配置使用攝像頭方向,同時可配置照片不用儲存到相簿中,禁用顯示相簿按鈕,保證使用者只能現場拍照,可以滿足專案需求。

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

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:'圖片預覽失敗'
                            })
                        }
                });
            },

關於使用者頭像的設定,使用者可選擇拍照和從相簿中選擇照片。同時支援裁剪以滿足使用者頭像設定的需求。裁剪用到的是FNImageClip模組。在使用FNImageClip模組的時候建議新開frame頁面,在新的frame頁面進行裁剪操作,裁剪完成之後通過推送事件監聽來更新頭像!

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

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
                                }
                            });
                        }
                    });
                });
            }
<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>

圖片預覽

專案中很多頁面涉及到圖片預覽的功能,分為單圖預覽和多圖預覽。圖片預覽採用的是photoBrowser 模組。

photoBrowser 是一個圖片瀏覽器,支援單張、多張圖片檢視的功能,可放大縮小圖片,支援本地和網路圖片資源。若是網路圖片資源則會被快取到本地,快取到本地上的資源可以通過 clearCache 介面手動清除。同時本模組支援橫豎屏顯示,在本app支援橫豎屏的情況下,本模組底層會自動監聽當前裝置的位置狀態,自動適配橫豎屏以展示圖片。使用此模組開發者看實現炫酷的圖片瀏覽器。
使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

<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>
//檢視大圖
            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();
                    }
                });
            }

清除快取

由於專案中有很多拍照,檢視照片,在使用的過程中,就會產生很多的快取,快取多了會導致應用反應變慢。所以在應用中增加了清楚快取的功能,用的是官方提供的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>
apiready(){
                //獲取APP快取 非同步返回結果:
                api.getCacheSize((ret) => {
                    this.data.cache = parseInt(ret.size/1024/1024).toFixed(1);
                });
            },
clearCache(){
    api.clearCache(() => {
        api.toast({
            msg: '清除完成'
        });
    });
    this.data.cache=0;
},

註冊頁面、傳送手機驗證碼

核心程式碼在 如何在傳送驗證碼成功之後,設定再次發動驗證碼倒數計時讀秒及禁用點選事件。

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

<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>

後臺系統

登陸介面、註冊介面、傳送手機驗證碼、列表查詢介面,其中手機簡訊用的是阿里的簡訊。

阿里簡訊的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();
      }  
    }
}

後臺系統頁面關於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>

主要用到了bootstrap的柵格佈局,作為頁面佈局的使用。

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

eaysui用的是1.5.3版本,用到了下圖中的這些控制元件。具體使用說明可以下載一個chm API使用手冊。

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

使用APICloud AVM多端框架開發企業移動OA辦公的專案實踐

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>

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>