本项目主要是针对企业内部员工使用,除了大部分OA办公常用的功能模块,也有部分定制化的功能模块。后台用的PHP+BootStrap+Easyui(PS:是不是感觉很久远的技术了)。
功能介绍
1、考勤打卡签到,加班打卡签到
2、办公流程申请、审批
3、通知下发、短信消息提醒
4、个人考勤记录查询,按月统计、钻取查询明细
思维导图
技术要点
Flex布局,amap地图应用,消息推送,短信提醒。
应用模块
项目目录
开发介绍
首页导航
系统首页使用tabLayout,可以将相关参数配置在JSON文件中,再在config.xml中将content的值设置成该JSON文件的路径。如果底部导航没有特殊需求这里强烈建议大家使用tabLayout为APP进行布局,官方已经将各类手机屏幕及不同的分辨率进行了适配,免去了很多关于适配方面的问题。
{
"name": "root",
"hideNavigationBar": false,
"bgColor": "#fff",
"navigationBar": {
"background": "#1492ff",
"shadow": "rgba(0,0,0,0)",
"color": "#fff",
"fontSize": 18,
"hideBackButton": true
},
"tabBar": {
"background": "#fff",
"shadow": "#eee",
"color": "#5E5E5E",
"selectedColor": "#1492ff",
"textOffset": 3,
"fontSize": 11,
"scrollEnabled": true,
"index": 0,
"preload": 1,
"frames": [{
"name": "home",
"url": "./pages/index/index.stml",
"title": "首页"
}, {
"name": "notice",
"url": "./pages/notice/notice.stml",
"title": "通知"
}, {
"name": "records",
"url": "./pages/records/records.stml",
"title": "记录"
}, {
"name": "user",
"url": "./pages/wode/wode.stml",
"title": "我的"
}],
"list": [{
"text": "首页",
"iconPath": "./images/toolbar/icon-home.png",
"selectedIconPath": "./images/toolbar/icon-home-selected.png"
}, {
"text": "通知",
"iconPath": "./images/toolbar/icon-notice.png",
"selectedIconPath": "./images/toolbar/icon-notice-selected.png"
}, {
"text": "记录",
"iconPath": "./images/toolbar/icon-records.png",
"selectedIconPath": "./images/toolbar/icon-records-selected.png"
}, {
"text": "我的",
"iconPath": "./images/toolbar/icon-user.png",
"selectedIconPath": "./images/toolbar/icon-user-selected.png"
}]
}
}
接口调用
将接口调用和接口配置分别封装了2个JS插件,model.js和config.js。这样来统一管理,避免了在每个页面进行接口调用的时候都重复写一遍代码,有效的简化了每个功能页面的代码量,只需要在回调里专注写自己的业务逻辑即可。
插件引用
import {Model} from "../../utils/model.js"
import {Config} from "../../utils/config.js"
config.js
class Config{
constructor(){}
}
Config.restUrl = 'http://127.0.0.1/index.php/Home/Api';
Config.queryrecordsbymonth ='/queryrecordsbymonth';//获取用户本月考勤记录
//省略
export {Config};
model.js
import {Config} from './config.js';
class Model {
constructor() {}
}
/*获取用户本月考勤记录 */
Model.queryrecordsbymonth = function (param, callback){
param.url = Config.queryrecordsbymonth;
param.method = 'post';
this.request(param, callback);
}
/*省略*/
Model.request = function(p, callback) {
var param = p;
if (!param.headers) {
param.headers = {};
}
// param.headers['x-apicloud-mcm-key'] = 'SZRtDyzM6SwWCXpZ';
if (param.data && param.data.body) {
param.headers['Content-Type'] = 'application/json; charset=utf-8';
}
if (param.url) {
param.url = Config.restUrl + param.url;
}
api.ajax(param, function(ret, err) {
callback && callback(ret, err);
});
}
export {Model};
页面中调用接口
//获取当前用户的本月考勤记录
recordsbymonth() {
const params = {
data:{
values:{
userid: api.getPrefs({sync: true,key: 'userid'}),
secret: Config.secret
}
}
}
Model.queryrecordsbymonth(params, (res,err) => {
console.log(JSON.stringify(res));
console.log(JSON.stringify(err));
if (res && res.flag == "Success") {
this.data.dk = res.data.dk;
this.data.cd = res.data.cd;
this.data.zt = res.data.zt;
this.data.tx = res.data.tx;
this.data.qj = res.data.qj;
}
else{
this.data.dk = 0;
this.data.cd = 0;
this.data.zt = 0;
this.data.tx = 0;
this.data.qj = 0;
}
api.hideProgress();
});
},
消息推送
消息推动采用了官方的push模块,因为产生消息提醒的事件都是在APP中进行触发,所有就用了官方的push模块;如果存在后台系统操作产生消息提醒的,官方的push模块就不适用了,需要用Jpush等三方消息推送平台模块,配合后台SDK进行消息推送。
用户绑定
//判断是否绑定推送
if(api.getPrefs({sync: true,key:'pushstatus'})!='02'){
var push = api.require('push');
push.bind({
userName: api.getPrefs({sync: true,key:'name'}),
userId: api.getPrefs({sync: true,key:'id'})
}, function(ret, err){
if( ret ){
// alert( JSON.stringify( ret) );
api.toast({
msg:'推送注册成功!'
});
//设置推送绑定状态,启动的时候判断一下
api.setPrefs({key:'pushstatus',value:'02'});
}else{
// alert( JSON.stringify( err) );
api.toast({
msg:'推送注册失败!'
})
api.setPrefs({key:'pushstatus',value:'01'});
}
});
}
推送消息
//发送抄送通知
copypush(){
const params = {
data:{
values:{
secret: Config.secret,
content:'有一条早晚加班申请已审批完成!'
}
}
}
Model.createcopytousermessage(params, (res,err) => {
// console.log(JSON.stringify(res));
// console.log(JSON.stringify(err));
if (res && res.flag == "Success") {
var users = res.data.join(',');
var now = Date.now();
var appKey = $sha1.sha1("A61542********" + "UZ" + "6B2246B9-A101-3684-5A34-67546C3545DA" + "UZ" + now) + "." + now;
api.ajax({
url : 'https://p.apicloud.com/api/push/message',
method : "post",
headers: {
"X-APICloud-AppId": "A615429********",
"X-APICloud-AppKey": appKey,
"Content-Type": "application/json"
},
dataType: "json",
data: {
"body": {
"title": "消息提醒",
"content": '有一条早晚加班申请已审批完成!',
"type": 2, //– 消息类型,1:消息 2:通知
"platform": 0, //0:全部平台,1:ios, 2:android
"userIds":users
}
}
}, (ret, err)=> {
// console.log(JSON.stringify(ret))
// console.log(JSON.stringify(err))
});
}
});
}
Flex布局
flex布局在AVM开发中是重中之重!还是那句话,flex布局写好,有CSS基础,根本就不需要用UI组件,完全可以实现UI的设计稿。
关于flex布局推荐一下阮一峰老师的教程,多读几遍多用,自然就会用的得心应手!上链接:https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html
通知公告
由于通知公告的内容是在后台通过富文本编辑器编辑的内容,其中会有样式布局的元素,不再是单纯的文字展示,这里使用了AVM中的rich-text组件,这个组件能很好的支持一些html元素标签,能完美的把富文本编辑的内容展现出来。
{this.data.title}
{this.data.author}|{this.data.sj}
数据列表及分页查询
数据列表的展示,采用scroll-view标签,通过onrefresherrefresh,onrefresherrefresh出发的事件中进行数据列表的刷新,和分页查询。refresher-triggered这个属性来设置当前下拉刷新状态,true 表示下拉刷新已经被触发,false 表示下拉刷新未被触发。如果想默认下拉刷新一下可以在apiready中将之设置为true,以此来代替执行数据刷新操作。
如果列表中的每一项的元素较少,而且没有样式的特殊要求,也可以使用list-view来实现。
下面是以通知公告列表的完整页面代码。其他页面的列表基本功能都是一致的,只是在每一项的样式及参数个数存在差异。
{{item.title}}
{{item.dt}}
{{item.author}}
{loadStateDesc}
组件开发
此项目中将模块缺省页和无数据页面封装为组件,方便在有数据查询的页面,不存在数据的情况直接引用组件即可。在事件项目需求中,尽量将通用的代码模块,封装成组件,这样不仅简化了页面代码量,而且很方便维护项目,组件中的内容修改一次,就可以应用到很多的使用组件的页面。
具体的开发教程可参考官方给出的教程并结合官方给出的点餐模板中的教程进行编写。这是官方链接:https://docs.apicloud.com/APICloud/Order-template-description
需要注意的点是,组件中使用installed,页面中使用apiready,如果组件中使用了apiready不会报错,但是不会执行你想要的结果。
地图模块使用
本应用中使用的是搞得地图amap,具体使用教程可通过模块使用教程进行详细了解,amp模块包含的功能特别丰富,基本上可以满足99%的关于地图的需求。
下面主要说明几点在使用高德地图过程中踩过的坑:
1、由于高德地图是原生模块,如果一个页面中地图只是其中一部分的元素的话,就需要注意地图的大小及位置,因为原生模块会遮罩页面元素,所以在固定好地图元素的位置之后,页面中的其他元素也要进行调整,我是用一个空白的view元素来占用地图组件的位置,然后在去调整其他页面的元素。
2、由于本项目中的考勤打卡是根据打卡位置进行了是否外勤的判断,正好用到了isCircleContainsPoint这个方法,但是需要注意的是,此方法只有在调用了open接口之后才有效,因为一开始就是做了一个根据经纬度查找地址信息,用到的getNameFromCoords不需要调用open接口即可。就没有调用open接口,导致后来用isCircleContainsPoint这个接口一直是无效的,都快整郁闷了!
3、新版本的高德地图应工信部要求,自本模块1.6.0版本起首次调用本模块前必须先弹出隐私协议,详情参考SDK合规使用方案。之后需先调用 updateMapViewPrivacy,updateSearchPrivacy,否则地图和搜索接口都无效。
如果你的项目之前用的是老版本的amap,后来打包的时候升级成最新的了,一定要加上这个两个接口!
var aMap = api.require('aMap');
aMap.open({
rect: {
x: 0,
y: 80,
h: api.frameHeight-300
},
showUserLocation: true,
showsAccuracyRing:true,
zoomLevel: 13,
center: {
lon: api.getPrefs({sync: true,key: 'lon'}),
lat: api.getPrefs({sync: true,key: 'lat'})
},
fixedOn: api.frameName,
fixed: true
}, (ret, err) => {
// console.log(JSON.stringify(ret));
// console.log(JSON.stringify(err));
if (ret.status) {
//获取用户位置 并判断是否在范围内500米
aMap.getLocation((ret, err) => {
if (ret.status) {
this.data.lon_now = ret.lon;
this.data.lat_now = ret.lat;
//解析当前地理位置
aMap.getNameFromCoords({
lon: ret.lon,
lat: ret.lat
}, (ret, err) => {
// console.log(JSON.stringify(ret));
if (ret.status) {
this.data.address=ret.address;
this.data.province = ret.state;
} else {
api.toast({
msg:'解析当前地理位置失败'
})
}
});
aMap.isCircleContainsPoint({
point: {
lon: api.getPrefs({sync: true,key: 'lon'}),
lat: api.getPrefs({sync: true,key: 'lat'})
},
circle: {
center: {
lon: ret.lon,
lat: ret.lat
},
radius: this.data.distance
}
}, (ret) => {
// console.log(JSON.stringify(ret));
if(ret.status){
this.data.isout=false;
this.data.btn_title='打卡签到';
}
else{
this.data.btn_title='外勤签到';
this.data.isout=true;
api.toast({
msg:'您不在考勤范围内'
})
}
});
} else {
api.toast({
msg:'定位失败,无法签到'
})
}
});
} else {
api.toast({
msg:'加载地图失败'
})
}
});
拍照及选择照片
因为项目考勤打卡需要每人每天拍3张照片,而且目前手机的像素较高,导致照片体积过大,严重消耗服务器内存;所以拍照使用的是FNPhotograph模块,自带UI的open接口,可选择拍照照片的质量,可配置使用摄像头方向,同时可配置照片不用存储到相册中,禁用显示相册按钮,保证用户只能现场拍照,可以满足项目需求。
openCamera (){
var FNPhotograph= api.require('FNPhotograph');
FNPhotograph.openCameraView({
rect: {
x: 0,
y: 80,
w: api.frameWidth,
h: api.frameHeight-70
},
orientation: 'portrait',
fixedOn: api.frameName,
useFrontCamera:true,//使用前置摄像头
fixed: true
}, (ret) => {
// console.log(JSON.stringify(ret));
if(ret.status){
this.data.istakephoto = true;
}
});
},
takephoto (){
var FNPhotograph= api.require('FNPhotograph');
FNPhotograph.takePhoto({
quality: 'low',
qualityValue:30,
path: 'fs://imagepath',
album: false
}, (ret) => {
// console.log(JSON.stringify(ret));
this.data.src = ret.imagePath;
FNPhotograph.closeCameraView((ret) => {
// console.log(JSON.stringify(ret));
if (ret.status) {
this.data.istakephoto = false;
this.data.isphoto = true;
}
});
});
},
showPicture (){
var photoBrowser = api.require('photoBrowser');
photoBrowser.open({
images: [
this.data.src
],
placeholderImg: 'widget://res/img/apicloud.png',
bgColor: '#000'
}, (ret, err) => {
if (ret) {
if(ret.eventType=='click'){
photoBrowser.close();
}
} else {
api.toast({
msg:'图片预览失败'
})
}
});
},
关于用户头像的设置,用户可选择拍照和从相册中选择照片。同时支持裁剪以满足用户头像设置的需求。裁剪用到的是FNImageClip模块。在使用FNImageClip模块的时候建议新开frame页面,在新的frame页面进行裁剪操作,裁剪完成之后通过推送事件监听来更新头像!
setavator(){
api.actionSheet({
cancelTitle: '取消',
buttons: ['拍照', '打开相册']
}, function(ret, err) {
if (ret.buttonIndex == 3) {
return false;
}
var sourceType = (ret.buttonIndex == 1) ? 'camera' : 'album';
api.getPicture({
sourceType: sourceType,
allowEdit: true,
quality: 20,
destinationType:'url',
targetWidth: 500,
targetHeight: 500
}, (ret, err) => {
if (ret && ret.data) {
$util.openWin({
name: 'facemake',
url: '../wode/facemake.stml',
title: '头像裁剪',
pageParam: {
faceimg:ret.data
}
});
}
});
});
}
取消
确定
重置
图片预览
项目中很多页面涉及到图片预览的功能,分为单图预览和多图预览。图片预览采用的是photoBrowser 模块。
photoBrowser 是一个图片浏览器,支持单张、多张图片查看的功能,可放大缩小图片,支持本地和网络图片资源。若是网络图片资源则会被缓存到本地,缓存到本地上的资源可以通过 clearCache 接口手动清除。同时本模块支持横竖屏显示,在本app支持横竖屏的情况下,本模块底层会自动监听当前设备的位置状态,自动适配横竖屏以展示图片。使用此模块开发者看实现炫酷的图片浏览器。
//查看大图
showPicture(e){
let url = e.currentTarget.dataset.url;
var urlarr= url.split(',');
var images=[];
urlarr.forEach(item => {
images.push(this.data.fileaddr+item);
});
// console.log(JSON.stringify(images));
var photoBrowser = api.require('photoBrowser');
photoBrowser.open({
images: images,
bgColor: '#000'
}, function(ret, err) {
if(ret.eventType=='click'){
photoBrowser.close();
}
});
}
清除缓存
由于项目中有很多拍照,查看照片,在使用的过程中,就会产生很多的缓存,缓存多了会导致应用反应变慢。所以在应用中增加了清楚缓存的功能,用的是官方提供的api.clearCache。
在个人中心 apiready中先获取到应用中的缓存,然后点击清除缓存按钮即可清除。
缓存
{cache}M
apiready(){
//获取APP缓存 异步返回结果:
api.getCacheSize((ret) => {
this.data.cache = parseInt(ret.size/1024/1024).toFixed(1);
});
},
clearCache(){
api.clearCache(() => {
api.toast({
msg: '清除完成'
});
});
this.data.cache=0;
},
注册页面、发送手机验证码
核心代码在 如何在发送验证码成功之后,设置再次发动验证码倒计时读秒及禁用点击事件。
获取验证码
{this.data.count}s
后台系统
登陆接口、注册接口、发送手机验证码、列表查询接口,其中手机短信用的是阿里的短信。
阿里短信的SDK通过 composer安装,在需要调用的php文件中头部引用即可。
table('user')
->field('id,name,phone,role,part as partid,user_num as usernum,usercenter,avator')->where($map)->find();
if($releaseInfo){
returnApiSuccess('登录成功',$releaseInfo);
}
else{
returnApiError( '登录失败,请稍后再试');
exit();
}
}
//用户注册
public function resigeruser(){
checkscret('secret');//验证授权码
checkdataPost('phone');//手机号
checkdataPost('password');//密码
checkdataPost('code');//验证码
$phone=$_POST['phone'];
$password=$_POST['password'];
$code=$_POST['code'];
//后台再次验证手机号码有效性
$ckphone=checkphone($phone);
if($ckphone=='T'){
$code_s=S($phone);
if($code_s==$code_s_s){
$data['phone']=$phone;
$data['password']=$password;
$data['role']='01';//注册用户
$data['resiger_time']=time();
$releaseInfo=M()->table('user')->data($data)->add();
if($releaseInfo){
//注销session
S($phone,'');
returnApiSuccess('注册成功',$releaseInfo);
}
else{
returnApiError( '注册失败,请稍后再试');
exit();
}
}
else{
returnApiError('验证码已失效,请重新获取');
exit();
}
}
else{
returnApiError('手机号已注册!');
exit();
}
}
//手机发送验证码
public function sendphonecode(){
checkscret('secret');//验证授权码
checkdataPost('phone');//手机号
$phone=trim($_POST['phone']);
$ckphone=checkphone($phone);
if($ckphone=='T'){//尚未注册手机号
//生成6位验证码
$code = substr(base_convert(md5(uniqid(md5(microtime(true)),true)), 16, 10), 0, 6);
//发送验证码
AlibabaCloud::accessKeyClient(C('accessKeyId'), C('accessSecret'))
->regionId('cn-beijing')
->asDefaultClient();
try {
$param = array("code"=>$code);
$result = AlibabaCloud::rpc()
->product('Dysmsapi')
// ->scheme('https') // https | http
->version('2022-01-25')
->action('SendSms')
->method('POST')
->host('dysmsapi.aliyuncs.com')
->options([
'query' => [
'RegionId' => "cn-beijing",
'PhoneNumbers' => $phone,
'SignName' => "*******有限公司",
'TemplateCode' => "SMS_*******",
'TemplateParam' => json_encode($param),
],
])
->request();
if($result['Code'] == 'OK'){
S($phone,$code,120);//设置一个120秒的过期时间
returnApiSuccess('发送成功',$result);
}
else{
returnApiError( '发送失败,请稍后再试');
exit();
}
} catch (ClientException $e) {
returnApiError( '发送失败,请稍后再试');
exit();
}
}
else{
returnApiError('手机号已注册!');
exit();
}
}
//查询用户加班记录
public function queryovertime(){
checkscret('secret');//验证授权码
checkdataPost('userid');//ID
checkdataPost('limit');//下一次加载多少条
$userid=$_POST['userid'];
//分页需要的参数
$limit=$_POST['limit'];
$skip=$_POST['skip'];
if(empty($skip)){
$skip=0;
}
//查询条件
$map['userid']=$userid;
$releaseInfo=M()->table('overtime_records')->field('id,kssj,ksrq,jsrq,ksbz,jsbz,jssj,kswz,jswz,kszp,jszp,zgsp,jlsp,xzsp,zgsp_time,jlsp_time')->where($map)->limit($limit*$skip,$limit)->order('kssj desc')->select();
if($releaseInfo){
returnApiSuccess('查询成功',$releaseInfo);
}
else{
returnApiSuccess('查询成功',[]);
exit();
}
}
}
后台系统页面关于easyui和bootstrap的引用
示例
主要用到了bootstrap的栅格布局,作为页面布局的使用。
eaysui用的是1.5.3版本,用到了下图中的这些控件。具体使用说明可以下载一个chm API使用手册。
html页面
js部分