CRM即“客户关系管理”,其载体是一种存储客户联系信息以及追踪客户活动的软件。在移动互联时代,CRM客户管理app更具实际价值,可以帮助企业摆脱PC的束缚、以更加灵活的方式开展业务,同时妥善地存储、更新全部客户信息,吸引新客户、保留老客户以及将已有客户转为忠实客户,实现业务增长。
本文案例来自开发者实战,讲解如何采用YonBuilder移动开发平台(APICloud)构建CRM客户管理app。
一、思维导图
二、功能介绍
1. 客户管理:录入客户信息、客户跟进、客户销售记录、直接拨打客户电话、条件筛选查询、公共客户;
2. 申请、收、发货管理;
3. 文档库、知识库;
4. 工作日志、日程管理;
5. 产品管理、库存管理;
6. 门店管理、员工管理;
7. 统计分析:客户统计分析、门店统计分析、员工统计分析、销售统计分析;
8. 通讯录、消息提醒;
9. 即时通讯、视频会议。
三、应用模块
四、项目目录
五、开发过程
1. 首页导航
系统首页使用tabLayout,可以将相关参数配置在JSON文件中,再在config.xml中将content的值设置成该JSON文件的路径。如果底部导航没有特殊需求这里强烈建议大家使用tabLayout为app进行布局,官方已经将各类手机屏幕及不同的分辨率进行了适配,免去了很多适配方面的问题。
{ "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": "个人中心" }], "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": "设置", "iconPath": "image/navbar/set-o.png", "selectedIconPath": "image/navbar/set.png", "scale":3 }] } }
2. 动态权限
在首页的apiready中根据提示授权需要获取的权限,app每次启动的时候就会判断是否已授权,如果未授权就提示进行授权。
apiready(){ let limits=[]; //获取权限 var resultList = api.hasPermission({ list: ['storage', 'location', 'camera', 'photos', 'phone'] }); if (resultList[0].granted) { // 已授权,可以继续下一步操作 } else { limits.push(resultList[0].name); } if (resultList[1].granted) { // 已授权,可以继续下一步操作 } else { limits.push(resultList[1].name); } if (resultList[2].granted) { // 已授权,可以继续下一步操作 } else { limits.push(resultList[2].name); } if (resultList[3].granted) { // 已授权,可以继续下一步操作 } else { limits.push(resultList[3].name); } if (resultList[4].granted) { // 已授权,可以继续下一步操作 } else { limits.push(resultList[4].name); } if(limits.length>0){ api.requestPermission({ list: limits, }, (res) => { }); } }
3. 消息事件
通过sendEvent把事件广播出去,然后在其他页面通过addEventListener监听事件,通过事件名和附带的参数进行其他操作。
举例:登录成功之后,需要在个人中心加载个人信息,在首页加载相关个人的数据;添加某项数据之后,需要进行刷新列表等等。
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:'登录失败!请稍后再试。' }) } api.hideProgress(); }).catch(err =>{ api.toast({ msg:JSON.stringify(err) }) }) } }
4. 接口调用
封装了req.js进行接口调用,采用了ES6语法中的Promise是异步编程的一种解决方案(比传统的回调函数更加合理、强大),用同步操作将异步流程表达出来,避免层层嵌套回调。Promise 对象提供统一接口,使得控制异步操作更加容易。有兴趣的同学可以多研究一下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请求快捷方法 * @constructor * @param url {string} 地址 * @param options {Object} 附加参数 */function GET(url, options = {}) { return req({ ...options, url, method: 'GET' });} /** * POST 请求快捷方法 * @param url * @param data * @param options {Object} 附加参数 * @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}
在页面中调用的时候首先需要引入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(){ //监听返回 双击退出程序 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模块。
具体使用方法可详细阅读官方模块文档。
推送功能初始化需要在app每次启动的时候进行集成,将初始化极光推送的方法集成在util工具类中,在首页进行初始化。
fnReadyAJpush(){ var jpush = api.require('ajpush'); api.addEventListener({name:'pause'}, function(ret,err) { onPause();//监听应用进入后台,通知jpush暂停事件 }) api.addEventListener({name:'resume'}, function(ret,err) { onResume();//监听应用恢复到前台,通知jpush恢复事件 }) //设置初始化 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: '绑定别名失败'}); } }); } //监听消息 jpush.setListener(function(ret) { var content = ret.content; alert(content); }); } else{ api.toast({ msg: '推送服务初始化失败'}); } }); }
初始化使用,每次启动app的时候需要,重新登陆之后可能存在切换账号的情况,也需要重新登陆。
8. 定位功能
因为系统中的定位只需要确定当前位置即可,所有定位功能使用的是aMapLBS模块,此模块没有打开地图的功能,只需要在用到的页面直接调用获取定位即可。
使用前需要在config.xml中进行配置,具体参数需要去高德开放平台去申请。
//获取当前位置信息和经纬度 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:'定位初始化失败,请开启手机定位。' }) return false; } }); }
9. 视频、语音通话
采用tencentTRTC开发音视频通话功能。需要先去腾讯云平台创建应用申请key,在通过官方提供的方法生成userSig。
生成userSig代码:
//获取腾讯视频RTC usersig public function getQQrtcusersig(){ checkscret('secret');//验证授权码 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( '查询失败,请稍后再试'); exit(); } }
用户视频画面需要根据当前视频用户数,进行计算调整。
<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">结束</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禁用左右滑动 api.setWinAttr({ slidBackEnabled: false }); api.addEventListener({ name:'keyback' }, (ret) =>{ //禁用返回按钮 }) api.addEventListener({ name: 'navbackbtn' }, (ret, err) => { api.confirm({ title: '提醒', msg: '你确定要离开本次会议吗?', buttons: ['确定', '继续'] },(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:'切换成功' }) } }) //视频模块监听事件 var tencentTRTC= api.require('tencentTRTC'); //view disappear 监听用户直接关闭APP的情况 默认把用户自己退出房间 api.addEventListener({name:'viewdisappear'},function(ret,err){ tencentTRTC.exitRoom({ }); }); //监听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'})){ //发起人离开房间 会议结束 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: '网络错误', duration: 2000, location: 'bottom' }); } }); //设置会议状态为开始 this.setRTCStatus('02'); } api.hideProgress(); }).catch(err =>{ api.toast({ msg:JSON.stringify(err) }) }) } }, //视频设置最多9个人,本人画面占一行,其他8人每行4个共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)); }, //设置会议状态 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'){ //在会议列表监听 刷新会议列表 已结束的不在显示 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实现通讯录功能,可直接拨打电话。
<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) }) }) }, //处理数据 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,所有图表相关的页面采用的是HTML,页面添加在HTML文件夹中,通过open.frame()进行打开。采用的Echarts.js.可去echarts官方下载,如果图标不复杂,建议使用自定义下载只选择用到的控件,减小文件体积;样式也可以自定义然后下载转述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>统计-客户</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(); }; //客户统计 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); } }) } //客户统计 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:'客户等级分析' }, tooltip : { trigger: 'item', formatter: "{a} <br/>{b} : {c} ({d}%)" }, series : [ { name: '客户等级占比', 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} ({d}%)" }, 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} ({d}%)" }, 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:'扫码成功,即将跳转详情页面' }) } } else{ api.toast({ msg:'扫码失败,请再次尝试!' }) } }); }
13. 数据列表及分页
数据列表的分页查询,主要使用到的是上拉刷新和下拉刷新动作,在动作的回调中处理需要的参数,来实现数据的加载和刷新。其中处理的参数需要配合后台提供的接口进行重定义。
<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">录入时间</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>跟进</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(){ //设置筛选按钮 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 '暂时没有内容'; } } }, 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: '客户跟进记录', 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. 导航栏底部出现“白边”问题处理
如果navigationBar的背景设置了其他颜色,shadow使用默认的颜色,如果页面背景设置成黑色,会有条明显的“白边”效果,这时需要通过设置shadow的颜色来消除“白边”。
(1)可在需要的页面通过setNavBarAttr进行设置,具体颜色值根据情况自行选择。
api.setNavBarAttr({ shadow:'#000000'});
(2)如果全局使用,则可在index.json中设置。
六、后台代码
<?phpnamespace HomeController;require 'vendor/autoload.php'; // 注意位置一定要在 引入ThinkPHP入口文件 之前use ThinkController;use JPushClient as JPushClient;class VideoController extends Controller { //查询视频会议列表 public function queryvideomeetinglist(){ checkscret('secret');//验证授权码 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(); } } //查询参加音视频会议人员列表 public function queryvideomeetingpersonlist(){ checkscret('secret');//验证授权码 $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(); } } //增加视频会议 public function addvideomeeting(){ checkscret('secret');//验证授权码 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){ //添加人员参加会议记录 foreach ($arrids as $v) { $datap['video_meeting_id']=$releaseInfo; $datap['userid']=$v; $res=M()->table('crm_video_audio_meeting_users')->data($datap)->add(); //推送视频会议消息 try{ //添加消息记录 $content='有一个视频会议需要您的参加,时间:'.$shijian; $datam['title']='视频会议通知'; $datam['content']=$content; $datam['shijian']=time(); $datam['flag']='01';//未读 $datam['type']='03';//会议提醒 $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') //机型 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(); } } //设置会议状态 public function setmeeting(){ checkscret('secret');//验证授权码 checkdataPost('id');//会议ID checkdataPost('flag');//会议状态 $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('设置成功',$releaseInfo); } else{ returnApiError( '设置失败!'); exit(); } } //获取会议信息 public function GetMeetingInfo(){ checkscret('secret');//验证授权码 checkdataPost('id');//会议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){ //获取与会人员 $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');//验证授权码 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( '查询失败,请稍后再试'); exit(); } }}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。