Bootstrap

APICloud AVM框架 开发CRM客户管理系统

CRM客户管理系统,通过信息技术以及互联网技术协调企业与顾客间在销售营销和服务上的交互,从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务的过程。其最终目标是吸引新客户、保留老客户以及将已有客户转为忠实客户,增加市场。

APP 开发采用APICloud AVM框架,后台采用PHP。

思维导图

功能介绍

1.客户管理:录入客户信息、客户跟进、客户销售记录、直接拨打客户电话、条件筛选查询、公共客户

2.申请、收、发货管理

3.文档库、知识库

4.工作日志、日程管理

5.产品管理、库存管理

6.门店管理、员工管理

7.统计分析:客户统计分析、门店统计分析、员工统计分析、销售统计分析

8.通讯录、消息提醒

9.即时通讯、视频会议

应用模块 

项目目录

开发介绍

首页导航

系统首页使用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
      }]
    }
  }

动态权限

在首页的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) => {
						
					});
				}
            }

消息事件

通过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)
					})
				})
			}
		}
            apiready(){
				api.addEventListener({
					name: 'loginsuccess'
				}, (ret, err) => {
					this.data.username = api.getPrefs({sync: true,key: 'name'});
					this.data.rolename = api.getPrefs({sync: true,key: 'rolename'});
					this.data.organname = api.getPrefs({sync: true,key: 'organname'});
				});
		     }

接口调用

封装了 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)
					})
				})
			}
        }

双击退出程序

在首页、登录页或其他需要双击退出程序的页面,在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
					});
				}
			});
          }

清空缓存

官方自带的API clearCache,可情况全部缓存,也可选择清除多少天前的缓存。

消息推送

采用极光推送,需要集成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的时候需要,重新登陆之后可能存在切换账号的情况,也需要重新登陆。

 

定位功能

因为系统中的定位只需要确定当前位置即可,所有定位功能使用的是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;
					}
				});
			}

视频、语音通话

采用tecnetRTC开发音视频通话功能。需要先去腾讯云平台创建应用申请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 \Tencent\TLSSigAPIv2($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>

通讯录

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

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>

扫描二维码 

模块文档中推荐了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:'扫码失败,请再次尝试!'
						})
					}
				});
			}

数据列表及分页

数据列表的分页查询,主要使用到的是上拉刷新和下拉刷新动作,在动作的回调中处理需要的参数,来实现数据的加载和刷新。其中处理的参数需要配个后台提供的接口进行重定义。

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

导航栏底部出现“白边”问题处理

 如果navigationBar的背景设置了其他颜色,shadow使用默认的颜色,如果页面背景设置成黑色,会有条明显的“白边”效果,这时需要通过设置shadow的颜色来消除“白边”。

1.可在需要的页面通过setNavBarAttr进行设置,具体颜色值根据情况自行选择。

api.setNavBarAttr({
    shadow:'#000000'
});

2.如果全局使用,则可在index.json中 设置。

后台代码

<?php
namespace Home\Controller;
require 'vendor/autoload.php';    // 注意位置一定要在 引入ThinkPHP入口文件 之前
use Think\Controller;
use JPush\Client 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 \Tencent\TLSSigAPIv2($sdkappid, $key);
      $sig = $api->genSig($userid);
      
      if($sig){
        returnApiSuccess('查询成功',$sig);
      }
      else{
        returnApiError( '查询失败,请稍后再试');
        exit();
      }
    }
}
<?php
namespace Home\Controller;
use Think\Controller;
class KnowController extends Controller {
    public function index(){
        $this->show('');
    }

     //查询知识
     public function QueryArticles(){
        $title=$_POST['title'];
        $type=$_POST['type'];
        if(!empty($title)){
           $map['title']=array('like','%'.$title.'%');
        }
        $map['type']=$type;
       //计算总共有多少页
       $page=$_POST['page'];
       $rows=$_POST['rows'];
       $data=M()->table('crm_article')->field('id,title,author,riqi,type,content')->where($map)->order('riqi desc,id desc')->limit($rows*($page-1),$rows)->select();
       $total=M('crm_article')->where($map)->count();
       $count=array();
       $count['total']=$total;
       $count['rows']=$data;

       $this->ajaxreturn($count);
    }

    //增加
    public function AddArticles(){
        $title=$_POST['title'];
        $author=$_POST['author'];
        $riqi=$_POST['fbsj'];
        $type=$_POST['type'];
        $content=$_POST['content'];

        $data['title']=$title;
        $data['author']=$author;
        $data['riqi']=$riqi;
        $data['type']=$type;
        $data['content']=$content;

        $info=M()->table('crm_article')->data($data)->add();
        if($info>0){
            $this->ajaxreturn('添加成功!');
        }
        else{
            $this->ajaxreturn('添加失败!');
        }
    }

    //修改
    public function EditArticles(){
        $id=$_POST['hidid'];
        $title=$_POST['title'];
        $author=$_POST['author'];
        $riqi=$_POST['fbsj'];
        $content=$_POST['content'];

        $map['id']=$id;

        $data['title']=$title;
        $data['author']=$author;
        $data['riqi']=$riqi;
        $data['content']=$content;

        $info=M()->table('crm_article')->where($map)->save($data);

        if($info>0){
            $this->ajaxreturn('修改成功!');
        }
        else{
            $this->ajaxreturn('修改失败!');
        }
    }

    //删除
    public function DelArticle(){
        $id=$_POST['id'];
        $map['id']=$id;
        $info= M()->table('crm_article')->where($map)->delete();
        if($info>0){
            $this->ajaxreturn('删除成功!');
        }
        else{
            $this->ajaxreturn('删除失败!');
        }
    }
}

;