CRM即“客戶關(guān)系管理”,其載體是一種存儲(chǔ)客戶聯(lián)系信息以及追蹤客戶活動(dòng)的軟件。在移動(dòng)互聯(lián)時(shí)代,CRM客戶管理app更具實(shí)際價(jià)值,可以幫助企業(yè)擺脫P(yáng)C的束縛、以更加靈活的方式開展業(yè)務(wù),同時(shí)妥善地存儲(chǔ)、更新全部客戶信息,吸引新客戶、保留老客戶以及將已有客戶轉(zhuǎn)為忠實(shí)客戶,實(shí)現(xiàn)業(yè)務(wù)增長。
本文案例來自開發(fā)者實(shí)戰(zhàn),講解如何采用YonBuilder移動(dòng)開發(fā)平臺(tái)(APICloud)構(gòu)建CRM客戶管理app。
一、思維導(dǎo)圖
二、功能介紹
1. 客戶管理:錄入客戶信息、客戶跟進(jìn)、客戶銷售記錄、直接撥打客戶電話、條件篩選查詢、公共客戶;
2. 申請(qǐng)、收、發(fā)貨管理;
3. 文檔庫、知識(shí)庫;
4. 工作日志、日程管理;
5. 產(chǎn)品管理、庫存管理;
6. 門店管理、員工管理;
7. 統(tǒng)計(jì)分析:客戶統(tǒng)計(jì)分析、門店統(tǒng)計(jì)分析、員工統(tǒng)計(jì)分析、銷售統(tǒng)計(jì)分析;
8. 通訊錄、消息提醒;
9. 即時(shí)通訊、視頻會(huì)議。
三、應(yīng)用模塊
四、項(xiàng)目目錄
五、開發(fā)過程
1. 首頁導(dǎo)航
系統(tǒng)首頁使用tabLayout,可以將相關(guān)參數(shù)配置在JSON文件中,再在config.xml中將content的值設(shè)置成該JSON文件的路徑。如果底部導(dǎo)航?jīng)]有特殊需求這里強(qiáng)烈建議大家使用tabLayout為app進(jìn)行布局,官方已經(jīng)將各類手機(jī)屏幕及不同的分辨率進(jìn)行了適配,免去了很多適配方面的問題。
{ "name": "root", "hideNavigationBar": true, "navigationBar": { "background": "#035dff", "color": "#fff", "shadow": "#035dff", "hideBackButton": true }, "tabBar": { "scrollEnabled": false, "background": "#fff", "shadow": "#f1f1f1", "color": "#8a8a8a", "selectedColor": "#000000", "index":0, "preload": 0, "frames": [{ "name": "home", "url": "pages/main/home.stml", "title": "主頁" }, { "name": "notice", "url": "pages/notice/notice-index.stml", "title": "消息通知" }, { "name": "tellbook", "url": "pages/main/tellbook.stml", "title": "通訊錄" }, { "name": "my", "url": "pages/seeting/my.stml", "title": "個(gè)人中心" }], "list": [{ "text": "主頁", "iconPath": "image/navbar/home-o.png", "selectedIconPath": "image/navbar/home.png", "scale":3 }, { "text": "提醒", "iconPath": "image/navbar/notice-o.png", "selectedIconPath": "image/navbar/notice.png", "scale":3 }, { "text": "通訊錄", "iconPath": "image/navbar/book-o.png", "selectedIconPath": "image/navbar/book.png", "scale":3 }, { "text": "設(shè)置", "iconPath": "image/navbar/set-o.png", "selectedIconPath": "image/navbar/set.png", "scale":3 }] } }
2. 動(dòng)態(tài)權(quán)限
在首頁的apiready中根據(jù)提示授權(quán)需要獲取的權(quán)限,app每次啟動(dòng)的時(shí)候就會(huì)判斷是否已授權(quán),如果未授權(quán)就提示進(jìn)行授權(quán)。
apiready(){ let limits=[]; //獲取權(quán)限 var resultList = api.hasPermission({ list: ['storage', 'location', 'camera', 'photos', 'phone'] }); if (resultList[0].granted) { // 已授權(quán),可以繼續(xù)下一步操作 } else { limits.push(resultList[0].name); } if (resultList[1].granted) { // 已授權(quán),可以繼續(xù)下一步操作 } else { limits.push(resultList[1].name); } if (resultList[2].granted) { // 已授權(quán),可以繼續(xù)下一步操作 } else { limits.push(resultList[2].name); } if (resultList[3].granted) { // 已授權(quán),可以繼續(xù)下一步操作 } else { limits.push(resultList[3].name); } if (resultList[4].granted) { // 已授權(quán),可以繼續(xù)下一步操作 } else { limits.push(resultList[4].name); } if(limits.length>0){ api.requestPermission({ list: limits, }, (res) => { }); } }
3. 消息事件
通過sendEvent把事件廣播出去,然后在其他頁面通過addEventListener監(jiān)聽事件,通過事件名和附帶的參數(shù)進(jìn)行其他操作。
舉例:登錄成功之后,需要在個(gè)人中心加載個(gè)人信息,在首頁加載相關(guān)個(gè)人的數(shù)據(jù);添加某項(xiàng)數(shù)據(jù)之后,需要進(jìn)行刷新列表等等。
methods: { login(){ if (!this.data.username) { this.showToast("姓名不能為空"); return; } if (!this.data.password) { this.showToast("密碼不能為空"); return; } var data={ secret:'', user:this.data.username, psw:this.data.password }; api.showProgress(); POST('Index/queryuserinfo',data,{}).then(ret =>{ // console.log(JSON.stringify(ret)); if(ret.flag=='Success'){ api.setPrefs({key:'username',value:ret.data.username}); //api.setPrefs({key:'password',value:ret.data.password}); api.setPrefs({key:'userid',value:ret.data.id}); api.setPrefs({key:'roleid',value:ret.data.roleid}); api.setPrefs({key:'rolename',value:ret.data.rolename}); api.setPrefs({key:'organid',value:ret.data.organid}); api.setPrefs({key:'organname',value:ret.data.organname}); api.setPrefs({key:'organtype',value:ret.data.organtype}); api.setPrefs({key:'phone',value:ret.data.phone}); api.setPrefs({key:'name',value:ret.data.name}); api.sendEvent({ name: 'loginsuccess', }); api.closeWin(); } else{ api.toast({ msg:'登錄失敗!請(qǐng)稍后再試。' }) } api.hideProgress(); }).catch(err =>{ api.toast({ msg:JSON.stringify(err) }) }) } }
4. 接口調(diào)用
封裝了req.js進(jìn)行接口調(diào)用,采用了ES6語法中的Promise是異步編程的一種解決方案(比傳統(tǒng)的回調(diào)函數(shù)更加合理、強(qiáng)大),用同步操作將異步流程表達(dá)出來,避免層層嵌套回調(diào)。Promise 對(duì)象提供統(tǒng)一接口,使得控制異步操作更加容易。有興趣的同學(xué)可以多研究一下Promise。
const config = { schema: 'http', host: '192.168.1.5', path: 'api.php/Home', secret:'776eca99-******-11e9-9897-*******'} function req(options) { const baseUrl = `${config.schema}://${config.host}/${config.path}/`; options.url = baseUrl options.url; return new Promise((resolve, reject) => { api.ajax(options, (ret, err) => { // console.log('[' options.method '] ' options.url ' [' api.winName '/' api.frameName ']n' JSON.stringify({ // ...options, ret, err // })) if (ret) { resolve(ret); api.hideProgress(); } else { reject(err); api.hideProgress(); } }); })}/** * GET請(qǐng)求快捷方法 * @constructor * @param url {string} 地址 * @param options {Object} 附加參數(shù) */function GET(url, options = {}) { return req({ ...options, url, method: 'GET' });} /** * POST 請(qǐng)求快捷方法 * @param url * @param data * @param options {Object} 附加參數(shù) * @returns {Promise<Object>} * @constructor */function POST(url, data, options = {}) { data.secret = config.secret; return req({ ...options, url, method: 'POST', data: { values: data } });} export { req, GET, POST, config}
在頁面中調(diào)用的時(shí)候首先需要引入js文件。
//引入 import {POST, GET} from '../../script/req.js' //使用 methods: { loadDaily(){ var data={ secret:'', userid: api.getPrefs({sync: true,key: 'userid'}) }; api.showProgress(); POST('Index/queryleastremind',data,{}).then(ret =>{ // console.log(JSON.stringify(ret)); if(ret.flag=='Success'){ this.data.dailyList = ret.data; this.data.isDaily = false; } else{ this.data.isDaily = true; } api.hideProgress(); }).catch(err =>{ this.data.isDaily = true; api.toast({ msg:JSON.stringify(err) }) }) } }
5. 雙擊退出程序
在首頁、登錄頁或其他需要雙擊退出程序的頁面,在apiready中添加。
apiready(){ //監(jiān)聽返回 雙擊退出程序 api.setPrefs({ key: 'time_last', value: '0' }); api.addEventListener({ name : 'keyback' }, (ret, err) => { var time_last = api.getPrefs({sync: true,key: 'time_last'}); var time_now = Date.parse(new Date()); if (time_now - time_last > 2000) { api.setPrefs({key:'time_last',value:time_now}); api.toast({ msg : '再按一次退出APP', duration : 2000, location : 'bottom' }); } else { api.closeWidget({ silent : true }); } }); }
6. 清空緩存
官方自帶的API clearCache,可清空全部緩存,也可選擇清除多少天前的緩存。
7. 消息推送
采用極光推送,需要集成ajpush模塊。
具體使用方法可詳細(xì)閱讀官方模塊文檔。
推送功能初始化需要在app每次啟動(dòng)的時(shí)候進(jìn)行集成,將初始化極光推送的方法集成在util工具類中,在首頁進(jìn)行初始化。
fnReadyAJpush(){ var jpush = api.require('ajpush'); api.addEventListener({name:'pause'}, function(ret,err) { onPause();//監(jiān)聽?wèi)?yīng)用進(jìn)入后臺(tái),通知jpush暫停事件 }) api.addEventListener({name:'resume'}, function(ret,err) { onResume();//監(jiān)聽?wèi)?yīng)用恢復(fù)到前臺(tái),通知jpush恢復(fù)事件 }) //設(shè)置初始化 jpush.init(function(ret, err){ if(ret && ret.status){ var ali=$api.getStorage('userid'); var tag=$api.getStorage('roleid'); //綁定別名 if($api.getStorage('userid')){ jpush.bindAliasAndTags({ alias:ali, tags:[tag] }, function(ret, err){ if(ret.statusCode==0){ api.toast({ msg: '推送初始化成功'}); } else{ api.toast({ msg: '綁定別名失敗'}); } }); } //監(jiān)聽消息 jpush.setListener(function(ret) { var content = ret.content; alert(content); }); } else{ api.toast({ msg: '推送服務(wù)初始化失敗'}); } }); }
初始化使用,每次啟動(dòng)app的時(shí)候需要,重新登陸之后可能存在切換賬號(hào)的情況,也需要重新登陸。
8. 定位功能
因?yàn)橄到y(tǒng)中的定位只需要確定當(dāng)前位置即可,所有定位功能使用的是aMapLBS模塊,此模塊沒有打開地圖的功能,只需要在用到的頁面直接調(diào)用獲取定位即可。
使用前需要在config.xml中進(jìn)行配置,具體參數(shù)需要去高德開放平臺(tái)去申請(qǐng)。
//獲取當(dāng)前位置信息和經(jīng)緯度 setLocation(){ var aMapLBS = api.require('aMapLBS'); aMap.updateLocationPrivacy({ privacyAgree:'didAgree', privacyShow:'didShow', containStatus:'didContain' }); aMapLBS.configManager({ accuracy: 'hundredMeters', filter: 1 }, (ret, err) => { if (ret.status) { aMapLBS.singleLocation({ timeout: 2 }, (ret, err) => { if (ret.status) { this.data.lon = ret.lon; this.data.lat = ret.lat; } }); aMapLBS.singleAddress({ timeout: 2 }, (ret, err) => { if (ret.status) { this.data.address = ret.formattedAddress; } }); } else{ api.toast({ msg:'定位初始化失敗,請(qǐng)開啟手機(jī)定位。' }) return false; } }); }
9. 視頻、語音通話
采用tencentTRTC開發(fā)音視頻通話功能。需要先去騰訊云平臺(tái)創(chuàng)建應(yīng)用申請(qǐng)key,在通過官方提供的方法生成userSig。
生成userSig代碼:
//獲取騰訊視頻RTC usersig public function getQQrtcusersig(){ checkscret('secret');//驗(yàn)證授權(quán)碼 checkdataPost('userid');//用戶ID $sdkappid=C('sdkappid'); $key=C('usersig_key'); $userid=$_POST['userid']; require 'vendor/autoload.php'; $api = new TencentTLSSigAPIv2($sdkappid, $key); $sig = $api->genSig($userid); if($sig){ returnApiSuccess('查詢成功',$sig); } else{ returnApiError( '查詢失敗,請(qǐng)稍后再試'); exit(); } }
用戶視頻畫面需要根據(jù)當(dāng)前視頻用戶數(shù),進(jìn)行計(jì)算調(diào)整。
<template> <view class="page"> <view class="video-bk"></view> <view class="footer"> <view class="footer-item" @click="setLoud"> <image class="footer-item-ico" src='../../image/loud-on.png' mode="widthFix" v-if="isLoud"></image> <image class="footer-item-ico" src='../../image/loud-off.png' mode="widthFix" v-else></image> <text class="footer-item-label">免提</text> </view> <view class="footer-item" @click="setRTC"> <image class="footer-item-ico" src='../../image/stop.png' mode="widthFix" v-if="isStart"></image> <image class="footer-item-ico" src='../../image/start.png' mode="widthFix" v-else></image> <text class="footer-item-label" v-if="isStart">結(jié)束</text> <text class="footer-item-label" v-else>開始</text> </view> <view class="footer-item" @click="setMute"> <image class="footer-item-ico" src='../../image/mute-on.png' mode="widthFix" v-if="isMute"></image> <image class="footer-item-ico" src='../../image/mute-off.png' mode="widthFix" v-else></image> <text class="footer-item-label">靜音</text> </view> </view> </view></template><script> import $util from '../../utils/utils.js' import {POST} from '../../script/req.js' export default { name: 'rtcvideo', apiready(){ this.data.roomId = api.pageParam.id; this.data.meetStart = api.pageParam.userid; var tencentTRTC= api.require('tencentTRTC'); api.setNavBarAttr({ shadow:'#000000' }); //IOS禁用左右滑動(dòng) api.setWinAttr({ slidBackEnabled: false }); api.addEventListener({ name:'keyback' }, (ret) =>{ //禁用返回按鈕 }) api.addEventListener({ name: 'navbackbtn' }, (ret, err) => { api.confirm({ title: '提醒', msg: '你確定要離開本次會(huì)議嗎?', buttons: ['確定', '繼續(xù)'] },(ret)=>{ var index = ret.buttonIndex; if(index==1){ tencentTRTC.exitRoom({ }); api.closeWin(); } }) }); //添加右鍵切換攝像頭 api.setNavBarAttr({ rightButtons: [{ iconPath:'widget://image/switch.png' }] }); api.addEventListener({ name:'navitembtn' }, (ret)=>{ if(ret.type=='right'){ //切換前后攝像頭 tencentTRTC.switchCamera({ }); api.toast({ msg:'切換成功' }) } }) //視頻模塊監(jiān)聽事件 var tencentTRTC= api.require('tencentTRTC'); //view disappear 監(jiān)聽用戶直接關(guān)閉APP的情況 默認(rèn)把用戶自己退出房間 api.addEventListener({name:'viewdisappear'},function(ret,err){ tencentTRTC.exitRoom({ }); }); //監(jiān)聽RTC事件 tencentTRTC.setTRTCListener({},(ret, err) => { // console.log(JSON.stringify(ret)); // console.log(JSON.stringify(err)); if(ret.status){ if(ret.action=='enterRoom'){ //開啟語音 tencentTRTC.startLocalAudio({ }); tencentTRTC.startLocalPreview({ rect:{ x: 0, y: api.safeArea.top 45, w: api.frameWidth, h: api.frameHeight/3 } }); } //有用戶加入房間 else if(ret.action=='remoteUserEnterRoom'){ // console.log(this.data.rectindex); if(this.data.index>4) var thisRect = { x: (api.frameWidth/4)*this.data.rectindex, y: api.frameHeight/3 api.safeArea.top 45, w: api.frameWidth/4, h: api.frameWidth/4 } tencentTRTC.startRemoteView({ rect:thisRect, remoteUid:ret.remoteUserEnterRoom.userId, }); this.data.rectindex ; } //有用戶離開房間 else if(ret.action=='remoteUserLeaveRoom'){ tencentTRTC.stopRemoteView({ remoteUid:ret.remoteUserLeaveRoom.userId, }); } } else{ api.toast({ msg:JSON.stringify(err) }) } }); }, data() { return{ isMute:false, isLoud:false, isStart:false, rects:[], rectindex:0, roomId:'', meetStart:'', mTop:api.safeArea.top 45 } }, methods: { setMute(){ var tencentTRTC= api.require('tencentTRTC'); this.data.isMute = !this.data.isMute; tencentTRTC.muteLocalAudio({ mute:this.data.isMute }); }, setLoud(){ this.data.isLoud = !this.data.isLoud; }, setRTC(){ var tencentTRTC= api.require('tencentTRTC'); if(this.data.isStart){ if(this.data.meetStart == api.getPrefs({sync: true,key: 'userid'})){ //發(fā)起人離開房間 會(huì)議結(jié)束 this.setRTCStatus('03'); } tencentTRTC.exitRoom({ }); api.closeWin(); } else{ var data={ secret:'', userid: api.getPrefs({sync: true,key: 'userid'}) }; api.showProgress(); POST('Video/getQQrtcusersig',data,{}).then(ret =>{ console.log(JSON.stringify(ret)); if(ret.flag=='Success'){ this.data.isStart = !this.data.isStart; tencentTRTC.enterRoom({ appId:14*******272, userId:api.getPrefs({sync: true,key: 'userid'}), roomId:this.data.roomId, userSig:ret.data, scene:1 },(ret, err) => { // console.log(JSON.stringify(ret)); // console.log(JSON.stringify(err)); if(ret.result<0){ api.toast({ msg: '網(wǎng)絡(luò)錯(cuò)誤', duration: 2000, location: 'bottom' }); } }); //設(shè)置會(huì)議狀態(tài)為開始 this.setRTCStatus('02'); } api.hideProgress(); }).catch(err =>{ api.toast({ msg:JSON.stringify(err) }) }) } }, //視頻設(shè)置最多9個(gè)人,本人畫面占一行,其他8人每行4個(gè)共2行 setUserRect(){ for(var i=0;i<8;i ){ if(i<4){ this.data.rects[i]={ x: (api.frameWidth/4)*i, y: api.frameHeight/3 this.data.mTop, w: api.frameWidth/4, h: api.frameWidth/4 }; } else{ this.data.rects[i]={ x: (api.frameWidth/4)*(i-4), y: (api.frameHeight/3) (api.frameWidth/4) this.data.mTop, w: api.frameWidth/4, h: api.frameWidth/4 }; } } // console.log(JSON.stringify(this.data.rects)); }, //設(shè)置會(huì)議狀態(tài) setRTCStatus(status){ var data={ secret:'', id: this.data.roomId, flag:status }; api.showProgress(); POST('Video/setmeeting',data,{}).then(ret =>{ console.log(JSON.stringify(ret)); if(ret.flag=='Success'){ //在會(huì)議列表監(jiān)聽 刷新會(huì)議列表 已結(jié)束的不在顯示 api.sendEvent({ name: 'setmeeting' }); } api.hideProgress(); }).catch(err =>{ api.toast({ msg:JSON.stringify(err) }) }) } } }</script><style> .page { height: 100%; justify-content: space-between; background-color: #ffffff; } .video-bk{ height: 100%; background-color: #000000; } .footer{ height: 70px; background-color: #ffffff; flex-flow: row nowrap; justify-content: space-around; margin-bottom: 30px; align-items: center; margin-top: 20px; } .footer-item{ align-items: center; justify-content: center; } .footer-item-ico{ width: 45px; } .footer-item-label{ font-size: 13px; }</style>
10. 通訊錄
基于scroll-view實(shí)現(xiàn)通訊錄功能,可直接撥打電話。
<template> <view> <scroll-view class="page" scroll-y show-scrollbar="false" id="book"> <safe-area></safe-area> <view class="item" v-for="(item, index) in list" v-show="item.children.length>0"> <view class="nav" id={item.zkey}> <text class="nav-title">{item.zkey}</text> </view> <view class="box" v-for="(it, pindex) in item.children" data-phone={it.phone} @click="takePhone"> <image class="avator" src='../../image/avator.png' mode="widthFix"></image> <view class="right"> <text class="name">{it.remark}</text> <view class="bt"> <text class="bt-position">{it.position}</text> <text class="bt-part">{it.dept_name}</text> </view> </view> </view> </view> </scroll-view> <scroll-view class="right-nav" scroll-y show-scrollbar="false"> <view class="right-nav-item" data-id={item.zkey} @click="scrollToE" v-for="(item, index) in list"> <text class={item.zkey==zIndex?'right-nav-item-on':'right-nav-item-off'}>{item.zkey}</text> </view> </scroll-view> </view> </template><script> import {POST} from '../../script/req.js' export default { name: 'tellbook', apiready(){ this.loadData(); }, data() { return{ list:[], zIndex:'' } }, methods: { loadData(){ var data={ secret:'', userid:api.getPrefs({sync: true,key: 'userid'}) }; api.showProgress(); POST('Index/gettellbook',data,{}).then(ret =>{ if(ret.flag=='Success'){ this.toTree(ret.data); } api.hideProgress(); }).catch(err =>{ api.toast({ msg:JSON.stringify(err) }) }) }, //處理數(shù)據(jù) toTree(data){ var book=[]; var zm= 'A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z'.split(','); zm.forEach(element => { var arrz = data.filter((item) => { return item.zkey == element }) book.push({'zkey':element,children:arrz}); }); this.data.list = book; //console.log(JSON.stringify(book)); }, scrollToE(e){ var id = e.target.dataset.id; var book = document.getElementById('book'); book.scrollTo({ view:id }) this.data.zIndex = id; }, takePhone(e){ var phone = e.target.dataset.phone; api.call({ type: 'tel', number: phone }); } } }</script><style> .page { height: 100%; background-color: #ffffff; } .nav{ margin: 0 10px; padding: 0 10px; } .nav-title{ font-size: 20px; } .box{ flex-flow: row nowrap; justify-content: flex-start; align-items: center; margin: 10px; border-bottom: 1px solid #ccc; padding-bottom: 10px; } .avator{ padding: 5px; } .right{ padding-left: 20px; } .bt{ flex-flow: row nowrap; justify-content: flex-start; align-items: center; } .bt-position{ font-size: 14px; color: #666666; } .bt-part{ font-size: 14px; color: #666666; padding-left: 20px; } .right-nav{ position: absolute; right: 10px; width: 30px; padding: 30px 0; height: 100%; align-items: center; } .right-nav-item{ padding-bottom: 5px; } .right-nav-item-on{ color: #035dff; } .right-nav-item-off{ color: #666666; } .avator{ width: 50px; }</style>
11. echarts圖表
由于AVM無法解析cavans,所有圖表相關(guān)的頁面采用的是HTML,頁面添加在HTML文件夾中,通過open.frame()進(jìn)行打開。采用的Echarts.js.可去echarts官方下載,如果圖標(biāo)不復(fù)雜,建議使用自定義下載只選擇用到的控件,減小文件體積;樣式也可以自定義然后下載轉(zhuǎn)述js文件。
文件目錄:
<!doctype html><html> <head> <meta charset="utf-8"> <meta name="viewport" content="maximum-scale=1.0,minimum-scale=1.0,user-scalable=0,width=device-width,initial-scale=1.0" /> <title>統(tǒng)計(jì)-客戶</title> <link rel="stylesheet" type="text/css" href="../css/api.css" /> <style> body{background:#efefef;padding: 10px 10px 50px 10px;} .chart-box{ background-color: #ffffff; border-radius: 5px; margin-top: 10px; }</style> </head> <body> <div class="chart-box"> <div id="Chart1" style="height:200px;"></div> </div> <div class="chart-box"> <div id="Chart2" style="height:200px;"></div> </div> <div class="chart-box"> <div id="Chart3" style="height:200px;"></div> </div> <div class="chart-box"> <div id="Chart4" style="height:200px;"></div> </div> </body> <script type="text/javascript" src="../script/api.js"></script> <script type="text/javascript" src="../script/echarts.min.js"></script> <script type="text/javascript" src="../script/customed.js"></script> <script type="text/javascript" src="../script/remotedb.js"></script> <script> apiready = function() { loaddemo1(); loaddemo2(); }; //客戶統(tǒng)計(jì) function loaddemo1(){ var path='http://192.168.1.5/api.php/Home/Statistic/querykhzzl'; var data={values:{ secret:'776eca99-****-11e9-******00163e008b45', year:'2021' }}; fnPost(path, data, function(ret, err) { // console.log(JSON.stringify(ret)); // console.log(JSON.stringify(err)); if(ret['flag']=='Success'){ var data=ret['data']; var arryaxis=[],arrzzl=[],arrall=[]; for(var i=0;i<data.length;i ){ arryaxis[i]=MonthToZhcn(data[i]['mon']); arrzzl[i]=data[i]['num']; arrall[i]=data[i]['monall']; } var myChart = echarts.init(document.getElementById('Chart1'),'customed'); var option = { title:{ text:'2021年客戶全年增長量和保有量' }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { data: ['增長客戶','客戶總量'], orient:'vertical', right:10, top:120 }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'value', boundaryGap: [0, 0.01] }, yAxis: { type: 'category', data: arryaxis }, series: [ { name: '增長客戶', type: 'bar', data: arrzzl }, { name: '客戶總量', type: 'bar', data: arrall } ] }; myChart.setOption(option); } }) } //客戶統(tǒng)計(jì) function loaddemo2(){ var datayear=[]; var path='http://192.168.1.5/api.php/Home/Statistic/querynvbl'; var data={values:{ secret:'776eca99-a1e5-11e9-9897-00163e008b45' }}; fnPost(path, data, function(ret, err) { // console.log(JSON.stringify(ret)); // console.log(JSON.stringify(err)); if(ret['flag']=='Success'){ var data=ret['data']; if(data['year']){ var year=data['year']; datayear.push({value:year['num1'],name:'18-20歲'}); datayear.push({value:year['num2'],name:'21-30歲'}); datayear.push({value:year['num3'],name:'31-40歲'}); datayear.push({value:year['num4'],name:'41-50歲'}); datayear.push({value:year['num5'],name:'51歲以上'}); } var myChart2 = echarts.init(document.getElementById('Chart2'),'customed'); var myChart3 = echarts.init(document.getElementById('Chart3'),'customed'); var myChart4 = echarts.init(document.getElementById('Chart4'),'customed'); option2 = { title:{ text:'客戶等級(jí)分析' }, tooltip : { trigger: 'item', formatter: "{a} <br/>{b} : {c} (lftbtjp5bp%)" }, series : [ { name: '客戶等級(jí)占比', type: 'pie', radius : '55%', center: ['50%', '60%'], data:data['csd'], itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] }; option3 = { title:{ text:'客戶性別分析' }, tooltip : { trigger: 'item', formatter: "{a} <br/>{b} : {c} (lftbtjp5bp%)" }, series : [ { name: '客戶性別占比', type: 'pie', radius : '55%', center: ['50%', '60%'], data:data['sex'], itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] }; option4 = { title:{ text:'客戶年齡分析' }, tooltip : { trigger: 'item', formatter: "{a} <br/>{b} : {c} (lftbtjp5bp%)" }, series : [ { name: '客戶年齡占比', type: 'pie', radius : '55%', center: ['50%', '60%'], data:datayear, itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] }; myChart2.setOption(option2); myChart3.setOption(option3); myChart4.setOption(option4); } }) }</script></html>
12. 掃描二維碼
模塊文檔中推薦了2種方式,如沒特殊需求,推薦使用第一種。
//入口<view class="column-item" @click="fnscanner"> <image class="column-item-ico" src='../../image/co-ico5.png' mode="widthFix"></image> <text class="column-item-title">掃碼</text></view> //使用 fnscanner(){ var FNScanner = api.require('FNScanner'); FNScanner.open({ autorotation: true }, (ret, err) => { console.log(JSON.stringify(ret)); console.log(JSON.stringify(err)); if(ret){ if(ret.eventType=='success'){ api.toast({ msg:'掃碼成功,即將跳轉(zhuǎn)詳情頁面' }) } } else{ api.toast({ msg:'掃碼失敗,請(qǐng)?jiān)俅螄L試!' }) } }); }
13. 數(shù)據(jù)列表及分頁
數(shù)據(jù)列表的分頁查詢,主要使用到的是上拉刷新和下拉刷新動(dòng)作,在動(dòng)作的回調(diào)中處理需要的參數(shù),來實(shí)現(xiàn)數(shù)據(jù)的加載和刷新。其中處理的參數(shù)需要配合后臺(tái)提供的接口進(jìn)行重定義。
<template> <scroll-view scroll-y class="page" enable-back-to-top refresher-enabled refresher-triggered={refresherTriggered} onrefresherrefresh={this.onrefresherrefresh} onscrolltolower={this.onscrolltolower}> <view> <view class="item-box" v-for="(item, index) in list" data-id={item.id}> <view class="top"> <image class="top-ico" src='../../image/flag01.png' mode="widthFix" v-if="item.flag=='01'"></image> <image class="top-ico" src='../../image/flag02.png' mode="widthFix" v-else-if="item.flag=='02'"></image> <image class="top-ico" src='../../image/flag03.png' mode="widthFix" v-else-if="item.flag=='03'"></image> <image class="top-ico" src='../../image/flag04.png' mode="widthFix" v-else-if="item.flag=='04'"></image> <image class="top-ico" src='../../image/flag05.png' mode="widthFix" v-else-if="item.flag=='05'"></image> <image class="top-ico" src='../../image/flag06.png' mode="widthFix" v-else></image> <text class="top-name">{item.name}</text> <text class="top-level">★{item.dengji}</text> </view> <view class="mid"> <view> <text class="mid-tip">錄入時(shí)間</text> <text>{item.lrsj}</text> </view> <view> <text class="mid-tip">生日</text> <text>{item.birthday}</text> </view> </view> <view class="btm"> <view class="btm-item" data-phone={item.phone} @click="call"> <image class="btm-ico" src='../../image/TELL.png' mode="widthFix"></image> <text>打電話</text> </view> <view class="btm-item" data-id={item.id} @click="followRecords"> <image class="btm-ico" src='../../image/GJ.png' mode="widthFix"></image> <text>跟進(jìn)</text> </view> <view class="btm-item" data-id={item.id} @click="saleRecords"> <image class="btm-ico" src='../../image/XS.png' mode="widthFix"></image> <text>銷售</text> </view> </view> </view> </view> <view class="footer"> <text class="loadDesc">{loadStateDesc}</text> </view> <safe-area></safe-area> </scroll-view></template><script> import $util from '../../utils/utils.js' import {POST} from '../../script/req.js' export default { name: 'customer', apiready(){ //設(shè)置篩選按鈕 api.setNavBarAttr({ rightButtons: [{ text:'篩選', color:'#ffffff' }] }); api.addEventListener({ name:'navitembtn' }, (ret)=>{ if(ret.type=='right'){ api.openFrame({ name: 'customer-select', url: 'customer-select.stml', rect: { x: 0, y: 0, w: 'auto', h: 'auto' }, pageParam: { name: 'test' } }); } }) api.addEventListener({ name:'doSearchCustomer' }, (ret)=>{ //重置key this.data.key = ''; // console.log(JSON.stringify(ret)); this.data.status = ret.value.status; this.data.level = ret.value.level; this.data.sex = ret.value.sex; this.loadData(); }) this.data.key = api.pageParam.key; //console.log(this.data.key); this.loadData(); }, data() { return{ key:'', list:[], skip: 0, refresherTriggered: false, haveMoreData: true, loading: false, status:'', level:'', sex:'' } }, computed: { loadStateDesc(){ if (this.data.loading || this.data.haveMoreData) { return '加載中...'; } else if (this.list.length > 0) { return '沒有更多啦'; } else { return '暫時(shí)沒有內(nèi)容'; } } }, methods: { loadData(loadMore) { this.data.loading = true; var limit = 10; var skip = loadMore?this.data.skip limit:0; var data={ secret:'', key:this.data.key, limit:limit, skip:skip, userid:api.getPrefs({sync: true,key: 'userid'}), roleid:api.getPrefs({sync: true,key: 'roleid'}), organid:api.getPrefs({sync: true,key: 'organid'}), }; api.showProgress(); POST('Customer/querycustomerlist',data,{}).then(ret =>{ // console.log(JSON.stringify(ret)); if(ret.flag=='Success'){ let noticedata = ret.data; this.data.haveMoreData = noticedata.length == limit; if (loadMore) { this.data.list = this.data.list.concat(noticedata); } else { this.data.list = noticedata; } this.data.skip = skip; } else{ this.data.haveMoreData = false; this.data.list=[]; } this.data.loading = false; this.data.refresherTriggered = false; api.hideProgress(); }) }, /*下拉刷新頁面*/ onrefresherrefresh(){ this.data.refresherTriggered = true; this.loadData(false); }, onscrolltolower() { if (this.data.haveMoreData) { this.loadData(true); } }, call(e){ var phone = e.target.dataset.phone; api.call({ type: 'tel', number: phone }); }, followRecords(e){ var id = e.target.dataset.id; $util.openWin({ name: 'followRecords', url: 'followRecords.stml', title: '客戶跟進(jìn)記錄', pageParam:{ id:id } }); }, saleRecords(e){ var id = e.target.dataset.id; $util.openWin({ name: 'saleRecords', url: 'saleRecords.stml', title: '客戶銷售記錄', pageParam:{ id:id } }); } } }</script><style> .page { height: 100%; background-color: #f0f0f0; } .item-box{ margin: 10px; background-color: #ffffff; border-radius: 5px; padding: 10px; } .top{ flex-flow: row nowrap; align-items: center; justify-content: space-between; } .mid{ flex-flow: row nowrap; align-items: center; justify-content: space-between; padding: 10px 0; } .mid-tip{ font-size: 13px; color: #666666; } .top-level{ color: #ffd700; } .top-ico{ width: 30px; } .top-name{ font-size: 20px; } .btm{ flex-flow: row nowrap; align-items: center; justify-content: space-between; padding-top: 10px; border-top: 1px solid #ccc; } .btm-item{ flex-flow: row nowrap; align-items: center; justify-content: center; } .btm-ico{ width: 20px; padding-right: 5px; } .footer { height: 44px; justify-content: center; align-items: center; } .loadDesc { width: 200px; text-align: center; }</style>
14. 導(dǎo)航欄底部出現(xiàn)“白邊”問題處理
如果navigationBar的背景設(shè)置了其他顏色,shadow使用默認(rèn)的顏色,如果頁面背景設(shè)置成黑色,會(huì)有條明顯的“白邊”效果,這時(shí)需要通過設(shè)置shadow的顏色來消除“白邊”。
(1)可在需要的頁面通過setNavBarAttr進(jìn)行設(shè)置,具體顏色值根據(jù)情況自行選擇。
api.setNavBarAttr({ shadow:'#000000'});
(2)如果全局使用,則可在index.json中設(shè)置。
六、后臺(tái)代碼
<?phpnamespace HomeController;require 'vendor/autoload.php'; // 注意位置一定要在 引入ThinkPHP入口文件 之前use ThinkController;use JPushClient as JPushClient;class VideoController extends Controller { //查詢視頻會(huì)議列表 public function queryvideomeetinglist(){ checkscret('secret');//驗(yàn)證授權(quán)碼 checkdataPost('limit');//下一次加載多少條 checkdataPost('userid');//用戶ID $limit=$_POST['limit']; $skip=$_POST['skip']; if(empty($skip)){ $skip=0; } $userid=$_POST['userid']; $map['_string']='(find_in_set('.$userid.',getvideomeetingusers(id)) and flag<>'03') or (userid='.$userid.' and flag<>'03')'; $releaseInfo=M()->table('crm_video_audio_meeting')->field('id,title,getcode_value('音視頻類型',type) lx,type,flag,getusername(userid) fqr,userid,estimatetime,type,note')->where($map)->limit($skip,$limit)->order('estimatetime desc')->select(); if($releaseInfo){ returnApiSuccess('查詢成功',$releaseInfo); } else{ returnApiError( '查詢失敗!'); exit(); } } //查詢參加音視頻會(huì)議人員列表 public function queryvideomeetingpersonlist(){ checkscret('secret');//驗(yàn)證授權(quán)碼 $releaseInfo=M()->table('crm_user')->field('id as employee_id,name,getorganname(organid) remark,getrolename(roleid) position,pinyin as phonetic')->where($map)->order('organid')->select(); if($releaseInfo){ returnApiSuccess('查詢成功',$releaseInfo); } else{ returnApiError( '查詢失敗!'); exit(); } } //增加視頻會(huì)議 public function addvideomeeting(){ checkscret('secret');//驗(yàn)證授權(quán)碼 checkdataPost('userid');//用戶ID $userid=$_POST['userid']; $title=$_POST['title']; $note=$_POST['note']; $shijian=$_POST['shijian']; $ids=$_POST['ids']; $arrids=explode(',',$ids); $data['userid']=$userid; $data['title']=$title; $data['note']=$note; $data['estimatetime']=$shijian; $data['estimatenum']=count($arrids); $data['type']=count($arrids)>9?'01':'02';//01 音頻 02 視頻 $data['flag']='01'; $releaseInfo=M()->table('crm_video_audio_meeting')->data($data)->add(); if($releaseInfo){ //添加人員參加會(huì)議記錄 foreach ($arrids as $v) { $datap['video_meeting_id']=$releaseInfo; $datap['userid']=$v; $res=M()->table('crm_video_audio_meeting_users')->data($datap)->add(); //推送視頻會(huì)議消息 try{ //添加消息記錄 $content='有一個(gè)視頻會(huì)議需要您的參加,時(shí)間:'.$shijian; $datam['title']='視頻會(huì)議通知'; $datam['content']=$content; $datam['shijian']=time(); $datam['flag']='01';//未讀 $datam['type']='03';//會(huì)議提醒 $datam['fqr']=$userid; $datam['jsr']=$v; $resm=M()->table('crm_message')->data($datam)->add(); $jpush = new JPushClient(C('JPUSH_APP_KEY'), C('JPUSH_MASTER_SECRET')); $response = $jpush->push() ->setPlatform('all') //機(jī)型 IOS ANDROID ->addAlias($v) ->androidNotification($content) ->iosNotification($content,'',0,true) ->options(array( 'apns_production' => true, )) ->send(); } catch(Exception $e){ returnApiSuccess('添加成功',$releaseInfo); } } returnApiSuccess('添加成功',$releaseInfo); } else{ returnApiError( '添加失敗!'); exit(); } } //設(shè)置會(huì)議狀態(tài) public function setmeeting(){ checkscret('secret');//驗(yàn)證授權(quán)碼 checkdataPost('id');//會(huì)議ID checkdataPost('flag');//會(huì)議狀態(tài) $flag=$_POST['flag']; $map['id']=$_POST['id']; $data['flag']=$flag; if($flag=='02'){ $data['starttime']=time(); } else if($flag=='03'){ $data['endtime']=time(); } $releaseInfo=M()->table('crm_video_audio_meeting')->where($map)->save($data); if($releaseInfo){ returnApiSuccess('設(shè)置成功',$releaseInfo); } else{ returnApiError( '設(shè)置失敗!'); exit(); } } //獲取會(huì)議信息 public function GetMeetingInfo(){ checkscret('secret');//驗(yàn)證授權(quán)碼 checkdataPost('id');//會(huì)議ID $id=$_POST['id']; $map['id']=$id; $releaseInfo=M()->table('crm_video_audio_meeting')->field('title,note,estimatetime,getusername(userid) fqr')->where($map)->find(); if($releaseInfo){ //獲取與會(huì)人員 $mapu['video_meeting_id']=$id; $datau=M()->table('crm_video_audio_meeting_users')->field('userid,getusername(userid) username')->where($mapu)->select(); if($datau){ $releaseInfo['users']=$datau; } returnApiSuccess('查詢成功',$releaseInfo); } else{ returnApiError( '查詢失敗!'); exit(); } } //獲取騰訊視頻RTC usersig public function getQQrtcusersig(){ checkscret('secret');//驗(yàn)證授權(quán)碼 checkdataPost('userid');//用戶ID $sdkappid=C('sdkappid'); $key=C('usersig_key'); $userid=$_POST['userid']; require 'vendor/autoload.php'; $api = new TencentTLSSigAPIv2($sdkappid, $key); $sig = $api->genSig($userid); if($sig){ returnApiSuccess('查詢成功',$sig); } else{ returnApiError( '查詢失敗,請(qǐng)稍后再試'); exit(); } }}
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。