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中添加。
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();
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。