CRM即“客户关系管理”,其载体是一种存储客户联系信息以及追踪客户活动的软件。在移动互联时代,CRM客户管理app更具实际价值,可以帮助企业摆脱PC的束缚、以更加灵活的方式开展业务,同时妥善地存储、更新全部客户信息,吸引新客户、保留老客户以及将已有客户转为忠实客户,实现业务增长。
本文案例来自开发者实战,讲解如何采用YonBuilder移动开发平台(APICloud)构建CRM客户管理app。
2.申请、收、发货管理;
3.文档库、知识库;
4.工作日志、日程管理;
5.产品管理、库存管理;
6.门店管理、员工管理;
7.统计分析:客户统计分析、门店统计分析、员工统计分析、销售统计分析;
8.通讯录、消息提醒;
9.即时通讯、视频会议。
1.首页导航
{
"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",
"text":"通讯录",
"iconPath":"image/navbar/book-o.png",
"selectedIconPath":"image/navbar/book.png",
"text":"设置",
"iconPath":"image/navbar/set-o.png",
"selectedIconPath":"image/navbar/set.png",
}]
}
2.动态权限
apiready(){
letlimits=[];
//获取权限
varresultList=api.hasPermission({
list:['storage','location','camera','photos','phone']
});
if(resultList[0].granted){
}else{
limits.push(resultList[0].name);
if(resultList[1].granted){
limits.push(resultList[1].name);
if(resultList[2].granted){
limits.push(resultList[2].name);
if(resultList[3].granted){
limits.push(resultList[3].name);
if(resultList[4].granted){
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("密码不能为空");
vardata={
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({
})
api.hideProgress();
}).catch(err=>{
msg:JSON.stringify(err)
4.接口调用
封装了req.js进行接口调用,采用了ES6语法中的Promise是异步编程的一种解决方案(比传统的回调函数更加合理、强大),用同步操作将异步流程表达出来,避免层层嵌套回调。Promise对象提供统一接口,使得控制异步操作更加容易。有兴趣的同学可以多研究一下Promise。
constconfig={
host:'192.168.1.5',
path:'api.php/Home',
secret:'776eca99-******-11e9-9897-*******'
functionreq(options){
constbaseUrl=`${config.schema}://${config.host}/${config.path}/`;
options.url=baseUrl+options.url;
returnnewPromise((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);
reject(err);
/**
*GET请求快捷方法
*@constructor
*@paramurl{string}地址
*@paramoptions{Object}附加参数
*/
functionGET(url,options={}){
returnreq({
...options,url,method:'GET'
*POST请求快捷方法
*@paramurl
*@paramdata
*@returns{Promise
functionPOST(url,data,options={}){
data.secret=config.secret;
...options,url,method:'POST',data:{
values:data
export{
req,GET,POST,config
在页面中调用的时候首先需要引入js文件。
//引入
import{POST,GET}from'../../script/req.js'
//使用
loadDaily(){
userid:api.getPrefs({sync:true,key:'userid'})
POST('Index/queryleastremind',data,{}).then(ret=>{
this.data.dailyList=ret.data;
this.data.isDaily=false;
this.data.isDaily=true;
5.双击退出程序
//监听返回双击退出程序
api.setPrefs({
key:'time_last',
value:'0'
api.addEventListener({
name:'keyback'
},(ret,err)=>{
vartime_last=api.getPrefs({sync:true,key:'time_last'});
vartime_now=Date.parse(newDate());
if(time_now-time_last>2000){
api.setPrefs({key:'time_last',value:time_now});
msg:'再按一次退出APP',
duration:2000,
location:'bottom'
api.closeWidget({
silent:true
6.清空缓存
官方自带的APIclearCache,可清空全部缓存,也可选择清除多少天前的缓存。
7.消息推送
采用极光推送,需要集成ajpush模块。
具体使用方法可详细阅读官方模块文档。
推送功能初始化需要在app每次启动的时候进行集成,将初始化极光推送的方法集成在util工具类中,在首页进行初始化。
fnReadyAJpush(){
varjpush=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){
varali=$api.getStorage('userid');
vartag=$api.getStorage('roleid');
//绑定别名
if($api.getStorage('userid')){
jpush.bindAliasAndTags({
alias:ali,
tags:[tag]
},function(ret,err){
if(ret.statusCode==0){
api.toast({msg:'推送初始化成功'});
api.toast({msg:'绑定别名失败'});
//监听消息
jpush.setListener(function(ret){
varcontent=ret.content;
alert(content);
api.toast({msg:'推送服务初始化失败'});
初始化使用,每次启动app的时候需要,重新登陆之后可能存在切换账号的情况,也需要重新登陆。
8.定位功能
因为系统中的定位只需要确定当前位置即可,所有定位功能使用的是aMapLBS模块,此模块没有打开地图的功能,只需要在用到的页面直接调用获取定位即可。
使用前需要在config.xml中进行配置,具体参数需要去高德开放平台去申请。
//获取当前位置信息和经纬度
setLocation(){
varaMapLBS=api.require('aMapLBS');
aMap.updateLocationPrivacy({
privacyAgree:'didAgree',
privacyShow:'didShow',
containStatus:'didContain'
aMapLBS.configManager({
accuracy:'hundredMeters',
filter:1
if(ret.status){
aMapLBS.singleLocation({
timeout:2
this.data.lon=ret.lon;
this.data.lat=ret.lat;
aMapLBS.singleAddress({
this.data.address=ret.formattedAddress;
msg:'定位初始化失败,请开启手机定位。'
returnfalse;
9.视频、语音通话
采用tencentTRTC开发音视频通话功能。需要先去腾讯云平台创建应用申请key,在通过官方提供的方法生成userSig。
生成userSig代码:
//获取腾讯视频RTCusersig
checkdataPost('userid');//用户ID
$sdkappid=C('sdkappid');
$key=C('usersig_key');
$userid=$_POST['userid'];
require'vendor/autoload.php';
$api=new\Tencent\TLSSigAPIv2($sdkappid,$key);
$sig=$api->genSig($userid);
if($sig){
returnApiSuccess('查询成功',$sig);
returnApiError('查询失败,请稍后再试');
exit();
用户视频画面需要根据当前视频用户数,进行计算调整。
import$utilfrom'../../utils/utils.js'
import{POST}from'../../script/req.js'
exportdefault{
name:'rtcvideo',
this.data.roomId=api.pageParam.id;
this.data.meetStart=api.pageParam.userid;
vartencentTRTC=api.require('tencentTRTC');
api.setNavBarAttr({
shadow:'#000000'
//IOS禁用左右滑动
api.setWinAttr({
slidBackEnabled:false
},(ret)=>{
//禁用返回按钮
name:'navbackbtn'
api.confirm({
title:'提醒',
msg:'你确定要离开本次会议吗?',
buttons:['确定','继续']
varindex=ret.buttonIndex;
if(index==1){
tencentTRTC.exitRoom({
//添加右键切换摄像头
rightButtons:[{
iconPath:'widget://image/switch.png'
name:'navitembtn'
if(ret.type=='right'){
//切换前后摄像头
tencentTRTC.switchCamera({
msg:'切换成功'
//视频模块监听事件
//viewdisappear监听用户直接关闭APP的情况默认把用户自己退出房间
api.addEventListener({name:'viewdisappear'},function(ret,err){
//监听RTC事件
tencentTRTC.setTRTCListener({},(ret,err)=>{
//console.log(JSON.stringify(err));
if(ret.action=='enterRoom'){
//开启语音
tencentTRTC.startLocalAudio({
tencentTRTC.startLocalPreview({
rect:{
x:0,
y:api.safeArea.top+45,
w:api.frameWidth,
h:api.frameHeight/3
//有用户加入房间
elseif(ret.action=='remoteUserEnterRoom'){
//console.log(this.data.rectindex);
if(this.data.index>4)
varthisRect={
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++;
//有用户离开房间
elseif(ret.action=='remoteUserLeaveRoom'){
tencentTRTC.stopRemoteView({
remoteUid:ret.remoteUserLeaveRoom.userId,
data(){
return{
isMute:false,
isLoud:false,
isStart:false,
rects:[],
rectindex:0,
roomId:'',
meetStart:'',
mTop:api.safeArea.top+45
setMute(){
this.data.isMute=!this.data.isMute;
tencentTRTC.muteLocalAudio({
mute:this.data.isMute
setLoud(){
this.data.isLoud=!this.data.isLoud;
setRTC(){
if(this.data.isStart){
if(this.data.meetStart==api.getPrefs({sync:true,key:'userid'})){
//发起人离开房间会议结束
this.setRTCStatus('03');
console.log(JSON.stringify(ret));
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
if(ret.result<0){
msg:'网络错误',
//设置会议状态为开始
this.setRTCStatus('02');
//视频设置最多9个人,本人画面占一行,其他8人每行4个共2行
setUserRect(){
for(vari=0;i<8;i++){
if(i<4){
this.data.rects[i]={
x:(api.frameWidth/4)*i,
y:api.frameHeight/3+this.data.mTop,
x:(api.frameWidth/4)*(i-4),
y:(api.frameHeight/3)+(api.frameWidth/4)+this.data.mTop,
//console.log(JSON.stringify(this.data.rects));
//设置会议状态
setRTCStatus(status){
id:this.data.roomId,
flag:status
POST('Video/setmeeting',data,{}).then(ret=>{
//在会议列表监听刷新会议列表已结束的不在显示
name:'setmeeting'
.page{
height:100%;
justify-content:space-between;
background-color:#ffffff;
.video-bk{
background-color:#000000;
.footer{
height:70px;
flex-flow:rownowrap;
justify-content:space-around;
margin-bottom:30px;
align-items:center;
margin-top:20px;
.footer-item{
justify-content:center;
.footer-item-ico{
width:45px;
.footer-item-label{
font-size:13px;
10.通讯录
name:'tellbook',
this.loadData();
list:[],
zIndex:''
loadData(){
POST('Index/gettellbook',data,{}).then(ret=>{
this.toTree(ret.data);
//处理数据
toTree(data){
varbook=[];
varzm='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=>{
vararrz=data.filter((item)=>{
returnitem.zkey==element
book.push({'zkey':element,children:arrz});
this.data.list=book;
//console.log(JSON.stringify(book));
scrollToE(e){
varid=e.target.dataset.id;
varbook=document.getElementById('book');
book.scrollTo({
view:id
this.data.zIndex=id;
takePhone(e){
varphone=e.target.dataset.phone;
api.call({
type:'tel',
number:phone
.nav{
margin:010px;
padding:010px;
.nav-title{
font-size:20px;
.box{
justify-content:flex-start;
margin:10px;
border-bottom:1pxsolid#ccc;
padding-bottom:10px;
.avator{
padding:5px;
.right{
padding-left:20px;
.bt{
.bt-position{
font-size:14px;
color:#666666;
.bt-part{
.right-nav{
position:absolute;
right:10px;
width:30px;
padding:30px0;
.right-nav-item{
padding-bottom:5px;
.right-nav-item-on{
color:#035dff;
.right-nav-item-off{
width:50px;
11.echarts图表
文件目录:
body{background:#efefef;padding:10px10px50px10px;}
.chart-box{
border-radius:5px;
margin-top:10px;