本我的项目次要是针对企业外部员工应用,除了大部分 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 元素标签,能完满的把富文本编辑的内容展示进去。
<template name='notice_info'>
<scroll-view class="main" scroll-y>
<text class="title">{this.data.title}</text>
<text class="subtitle">{this.data.author}|{this.data.sj}</text>
<rich-text class="content" nodes={this.data.content}></rich-text>
</scroll-view>
</template>
数据列表及分页查问
数据列表的展现,采纳 scroll-view 标签,通过 onrefresherrefresh,onrefresherrefresh 登程的事件中进行数据列表的刷新,和分页查问。refresher-triggered 这个属性来设置以后下拉刷新状态,true 示意下拉刷新曾经被触发,false 示意下拉刷新未被触发。如果想默认下拉刷新一下能够在 apiready 中将之设置为 true, 以此来代替执行数据刷新操作。
如果列表中的每一项的元素较少,而且没有款式的特殊要求,也能够应用 list-view 来实现。
上面是以告诉布告列表的残缺页面代码。其余页面的列表基本功能都是统一的,只是在每一项的款式及参数个数存在差别。
<template>
<scroll-view class="main" scroll-y enable-back-to-top refresher-enabled refresher-triggered={refresherTriggered} onrefresherrefresh={this.onrefresherrefresh} onscrolltolower={this.onscrolltolower}>
<view class="item-box">
<view class="item" data-id={item.id} onclick={this.openNoticeInfo} v-for="(item, index) in noticeList">
<text class="item-content">{{item.title}}</text>
<view class="item-sub">
<text class="item-info">{{item.dt}}</text>
<text class="item-info">{{item.author}}</text>
</view>
</view>
</view>
<view class="footer">
<text class="loadDesc">{loadStateDesc}</text>
</view>
</scroll-view>
</template>
<script>
import {Model} from '../../utils/model.js'
import {Config} from "../../utils/config.js"
import $util from "../../utils/util.js"
export default {
name: 'notice',
data() {
return{noticeList: [],
skip: 0,
loading: false,
refresherTriggered: false,
haveMoreData: true
}
},
computed: {loadStateDesc(){if (this.data.loading || this.data.haveMoreData) {return '加载中...';} else if (this.noticeList.length > 0) {return '没有更多啦';} else {return '临时没有内容';}
}
},
methods: {apiready(){
this.data.refresherTriggered = true;
this.loadData(false);
},
loadData(loadMore) {
this.data.loading = true;
var that = this;
var limit = 20;
var skip = loadMore?that.data.skip+1:0;
let params = {
data:{
values:{
secret: Config.secret,
userid: api.getPrefs({sync: true,key: 'userid'}),
skip: skip,
limit: limit
}
}
}
Model.getNoticeList(params, (res) => {if (res && res.flag == 'Success') {
let notices = res.data;
that.data.haveMoreData = notices.length == limit;
if (loadMore) {that.data.noticeList = that.data.noticeList.concat(notices);
} else {that.data.noticeList = notices;}
that.data.skip = skip;
} else {that.data.haveMoreData = false;}
that.data.loading = false;
that.data.refresherTriggered = false;
});
},
// 关上告诉详情页
openNoticeInfo: function (e) {
var id = e.currentTarget.dataset.id;
$util.openWin({
name: 'notice_info',
url: '../notice/notice_info.stml',
title: '告诉详情',
pageParam:{id:id}
});
},
/* 下拉刷新页面 */
onrefresherrefresh(){
this.data.refresherTriggered = true;
this.loadData(false);
},
onscrolltolower() {if (this.data.haveMoreData) {this.loadData(true);
}
}
}
}
</script>
<style>
.main {
height: 100%;
background-color: #f0f0f0;
}
.item-box{
background-color: #fff;
margin: 5px 5px;
}
.item{
border-bottom: 1px solid #efefef;
margin: 0 10px;
justify-content:flex-start;
flex-direction:column;
}
.item-content{
font-size: 17PX;
margin-top: 10px;
}
.item-info{
font-size: 13PX;
color: #666;
margin: 10px 0;
}
.item-sub{
justify-content:space-between;
flex-direction:row;
}
.footer {
height: 44px;
justify-content: center;
align-items: center;
}
.loadDesc {
width: 200px;
text-align: center;
}
</style>
组件开发
此我的项目中将模块缺省页和无数据页面封装为组件,不便在有数据查问的页面,不存在数据的状况间接援用组件即可。在事件我的项目需要中,尽量将通用的代码模块,封装成组件,这样不仅简化了页面代码量,而且很不便保护我的项目,组件中的内容批改一次,就能够利用到很多的应用组件的页面。
具体的开发教程可参考官网给出的教程并联合官网给出的点餐模板中的教程进行编写。这是官网链接: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}
});
}
});
});
}
<template name='facemake'>
<view class="page">
<view class="flowbottom">
<!-- <button class="btn-out" tapmode onclick="closeclip"> 勾销 </button>
<button class="btn" tapmode onclick="saveclip"> 确定 </button>
<button class="btn-off" tapmode onclick="resetclip"> 重置 </button> -->
<text class="btn-out" tapmode onclick="closeclip"> 勾销 </text>
<text class="btn" tapmode onclick="saveclip"> 确定 </text>
<text class="btn-off" tapmode onclick="resetclip"> 重置 </text>
</view>
</view>
</template>
<script>
import {Model} from "../../utils/model.js"
import {Config} from "../../utils/config.js"
export default {
name: 'facemake',
data() {
return{
facepic:'',
src:''
}
},
methods: {apiready(){//like created
// 获得图片地址
this.data.facepic=api.pageParam.faceimg;
FNImageClip = api.require('FNImageClip');
FNImageClip.open({
rect: {
x: 0,
y: 0,
w: api.winWidth,
h: api.winHeight-75
},
srcPath: this.data.facepic,
style: {
mask: '#999',
clip: {
w: 200,
h: 200,
x: (api.frameWidth-200)/2,
y: (api.frameHeight-275)/2,
borderColor: '#fff',
borderWidth: 1,
appearance: 'rectangle'
}
},
fixedOn: api.frameName
}, (ret, err) =>{// console.log(JSON.stringify(ret));
// console.log(JSON.stringify(err));
});
},
closeclip(){FNImageClip = api.require('FNImageClip');
FNImageClip.close();
api.closeWin();},
saveclip(){FNImageClip = api.require('FNImageClip');
FNImageClip.save({
destPath: 'fs://imageClip/result.png',
copyToAlbum: true,
quality: 1
},(ret, err)=>{// console.log(JSON.stringify(ret));
// console.log(JSON.stringify(err));
this.data.src = ret.destPath;
if(ret) {api.showProgress();
const params = {
data:{
values:{userid: api.getPrefs({sync: true,key: 'userid'}),
secret: Config.secret
},
files: {'file':[this.data.src]}
}
}
Model.updateuseravator(params, (res,err) => {// console.log(JSON.stringify(res));
// console.log(JSON.stringify(err));
if (res && res.flag == "Success") {
// 播送欠缺头像事件
api.sendEvent({
name: 'setavator',
extra: {key: res.data}
});
api.setPrefs({key:'avator',value:res.data});
api.closeWin();}
else{
api.toast({msg:'网络谬误,请稍后重试!'})
}
api.hideProgress();});
} else{
api.toast({msg:'网络谬误,请稍后重试!'})
}
});
},
resetclip(){FNImageClip = api.require('FNImageClip');
FNImageClip.reset();}
}
}
</script>
<style>
.page {
display: flex;
flex-flow: row nowrap;
height: 100%;
width: 100%;
}
.flowbottom{
width: 100%;
align-self: flex-end;
padding: 10px;
flex-flow: row nowrap;
justify-content: space-around;
}
.btn {
display: block;
height: 30px;
background:#1492ff;
border-radius: 5px;
color: #fff;
font-size: 16px;
padding: 5px 20px;
}
.btn-out {
display: block;
height: 30px;
background:#666;
border-radius: 5px;
color: #fff;
font-size: 16px;
padding: 5px 20px;
}
.btn-off {
display: block;
height: 30px;
background:#ec7d15;
border-radius: 5px;
color: #fff;
font-size: 16px;
padding: 5px 20px;
}
</style>
图片预览
我的项目中很多页面波及到图片预览的性能,分为单图预览和多图预览。图片预览采纳的是 photoBrowser 模块。
photoBrowser 是一个图片浏览器,反对单张、多张图片查看的性能,可放大放大图片,反对本地和网络图片资源。若是网络图片资源则会被缓存到本地,缓存到本地上的资源能够通过 clearCache 接口手动革除。同时本模块反对横竖屏显示,在本 app 反对横竖屏的状况下,本模块底层会主动监听以后设施的地位状态,主动适配横竖屏以展现图片。应用此模块开发者看实现炫酷的图片浏览器。
<view class="item-bottom" v-if="item.accessory">
<view v-for="p in item.accessory.split(',')" data-url={item.accessory} @click="showPicture">
<image class="item-bottom-pic" :src="this.data.fileaddr+p" mode="aspectFill"></image>
</view>
</view>
// 查看大图
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 中先获取到利用中的缓存,而后点击革除缓存按钮即可革除。
<view class="card_title" onclick="clearCache">
<image class="card_icon" src="../../images/icon/W_17.png" mode="scaleToFill"></image>
<text class="card_item"> 缓存 </text>
<text class="card_right_1">{cache}M</text>
</view>
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;
},
注册页面、发送手机验证码
外围代码在 如何在发送验证码胜利之后,设置再次动员验证码倒计时读秒及禁用点击事件。
<template name='register'>
<view class="page">
<view class="blank">
<image class="header" src="../../images/back/b_01.png" mode="scaleToFill"></image>
</view>
<view class="item-box">
<input class="item-input" placeholder="请输出 11 位手机号码" keyboard-type="tel" oninput="getPhone"/>
</view>
<view class="verification-code">
<input class="code-input" placeholder="输出验证码" keyboard-type="number" oninput="getCode"/>
<text v-show={this.data.show} class="code-btn" @click={this.sendCode}> 获取验证码 </text>
<text v-show={!this.data.show} class="code-btn">{this.data.count}s</text>
</view>
<view class="item-box">
<input class="item-input" placeholder="输出明码(6-20 位字符)" type="password" oninput="getPassword"/>
</view>
<view class="item-box">
<input class="item-input" placeholder="确认明码(6-20 位字符)" type="password" oninput="getPasswordAgain"/>
</view>
<view class="item-box">
<button class="btn" tapmode onclick="toresigter"> 注册 </button>
</view>
</view>
</template>
<script>
import {Model} from "../../utils/model.js"
import {Config} from "../../utils/config.js"
import $util from "../../utils/util.js"
export default {
name: 'register',
data() {
return{
show:true,
count: '',
timer: null,
phone:'',
code:'',
password:'',
passwordagain:''
}
},
methods: {apiready(){//like created},
getPhone(e){this.data.phone=e.detail.value;},
getCode(e){this.data.code=e.detail.value;},
getPassword(e){this.data.password=e.detail.value;},
getPasswordAgain(e){this.data.passwordagain=e.detail.value;},
sendCode(){if(this.data.phone==''||this.data.phone.length !=11){
api.toast({msg:'请填写正确的手机号!'})
return false;
}
const TIME_COUNT = 120;
if (!this.timer) {
this.count = TIME_COUNT;
this.show = false;
this.timer = setInterval(() => {if (this.count > 0 && this.count <= TIME_COUNT) {this.count--;} else {
this.show = true;
clearInterval(this.timer);
this.timer = null;
}
}, 1000)
}
// 后盾发送验证码
api.showProgress();
const params = {
data:{
values:{
phone: this.data.phone,
secret: Config.secret
}
}
}
Model.sendphonecode(params, (res,err) => {// console.log(JSON.stringify(res));
// console.log(JSON.stringify(err));
if (res && res.flag == "Success") {
api.toast({msg:'已发送,请留神查收'})
}
else{
api.toast({msg:res.msg});
}
api.hideProgress();});
},
toresigter(){if(this.data.phone=='' || this.data.phone.length !=11){
api.toast({msg:'请填写正确的 11 位手机号!'})
return false;
}
if(this.data.code==''){
api.toast({msg:'请填写验证码!'})
return false;
}
if(this.data.password==''){
api.toast({msg:'请填写新密码!'})
return false;
}
else{if(this.data.passwordagain==''){
api.toast({msg:'请填写确认明码!'})
return false;
}
else if(this.data.passwordagain != this.data.password){
api.toast({msg:'明码不统一!'})
return false;
}
}
api.showProgress();
const params = {
data:{
values:{
secret: Config.secret,
phone:this.data.phone,
pwd:this.data.password,
code:this.data.code
}
}
}
Model.resigeruser(params, (res,err) => {// console.log(JSON.stringify(res));
// console.log(JSON.stringify(err));
if (res && res.flag == "Success") {
api.alert({
title: '揭示',
msg: '注册胜利,行将跳转登陆',
}, function(ret, err) {api.closeWin();
});
}
else{
api.toast({msg:res.msg});
}
api.hideProgress();});
}
}
}
</script>
<style>
.page {
height: 100%;
width: 100%;
flex-flow: column;
justify-content: flex-start;
}
.blank{
height: 300px;
margin-bottom: 50px;
}
.header{
height: 300px;
width: 100%;
}
.item-box{
margin: 10px 20px;
border-bottom: 1px solid #f0f0f0;
}
.item-input{
height: 40px;
width: 100%;
border-radius: 5px;
border: none;
}
.verification-code{
flex-flow: row;
margin: 10px 20px;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.code-input{
height: 40px;
width: 70%;
border-radius: 5px;
border: none;
}
.code-btn{
height: 40px;
color: #1492ff;
}
.btn{
display: block;
width: 100%;
height: 50px;
background:#1492ff;
border-radius: 5px;
color: #fff;
font-size: 20px;
font-weight: bolder;
padding: 0;
margin-top: 10px;
}
</style>
后盾零碎
登陆接口、注册接口、发送手机验证码、列表查问接口,其中手机短信用的是阿里的短信。
阿里短信的 SDK 通过 composer 装置,在须要调用的 php 文件中头部援用即可。
<?php
namespace Home\Controller;
require 'vendor/autoload.php'; // 留神地位肯定要在 引入 ThinkPHP 入口文件 之前
use Think\Controller;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
class ApiController extends Controller {
// 用户登录
public function login(){checkscret('secret');// 验证受权码
checkdataPost('phone');// 手机号
checkdataPost('password');// 明码
$map['phone']=$_POST['phone'];
$map['password']=$_POST['password'];
$map['ischeck']='T';
$releaseInfo=M()->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 的援用
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> 示例 </title>
<!-- jquery - boot - 库文件 -->
<script src="__PUBLIC__/script/jquery.1.11.1.js"></script>
<script src="__PUBLIC__/script/bootstrap.min.js"></script>
<!-- Bootstrap -->
<link href="__PUBLIC__/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap -->
<!--easyui 蕴含文件 -->
<link rel="stylesheet" type="text/css" href="__PUBLIC__/plugins/easyui1.5.3/themes/material/easyui.css">
<link rel="stylesheet" type="text/css" href="__PUBLIC__/plugins/easyui1.5.3/themes/icon.css">
<script type="text/javascript" src="__PUBLIC__/plugins/easyui1.5.3/jquery.easyui.min.js"></script>
<script type="text/javascript" src="__PUBLIC__/plugins/easyui1.5.3/locale/easyui-lang-zh_CN.js"></script>
<!-- end easyui -->
<!--layer-->
<script type="text/javascript" src="__PUBLIC__/plugins/layer/layer.js"></script>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="__PUBLIC__/script/html5shiv.js"></script>
<script src="__PUBLIC__/script/respond.js"></script>
<![endif]-->
</head>
次要用到了 bootstrap 的栅格布局,作为页面布局的应用。
eaysui 用的是 1.5.3 版本,用到了下图中的这些控件。具体应用阐明能够下载一个 chm API 使用手册。
html 页面
<div class="container-fluid">
<div class="row">
<div class="col-md-12 mainbox" id="mainbox">
<!--menubegin-->
<div class="datamenubox" id="leftmenu">
<div class="menuhead">****</div>
<!-- treein -->
<div class="treein" id="menuin">
<ul class="list-group smenu">
<volist name="menulist" id="vo">
<a href="{:U($vo[url])}"><li class="list-group-item" id="{$vo.url}"><i class="fa fa-angle-right"></i>{$vo.name}</li></a>
</volist>
</ul>
</div>
</div>
<!--menuend-->
<!--mainboxbegin-->
<div class="col-md-12 rights" id="right">
<!-- 筛选 -->
<div class="searchitem">
<div class="row">
<div class="col-md-12">
<input class="easyui-combobox" name="q_user" id="q_user" style="width:200px" data-options="label:' 注销人:',valueField:'id',textField:'text',panelHeight:'180'">
<input class="easyui-textbox" name="q_cphm" id="q_cphm" style="width:200px" data-options="label:' 车牌号码:'">
<input class="easyui-datebox" name="q_ksrq" id="q_ksrq" style="width:200px" data-options="label:' 开始日期:'">
<input class="easyui-datebox" name="q_jsrq" id="q_jsrq" style="width:200px" data-options="label:' 完结日期:'">
</div>
</div>
<div class="blank10"></div>
<div class="row">
<div class="col-md-12">
<div class="btnin" id="normal">
<button class="btn btn-danger" id="querybtn"> 查问 </button>
<button class="btn btn-success" id="exportbtn"> 导出 Excel</button>
<button class="btn btn-info" id="delbtn"> 删除 </button>
</div>
<div class="btnin" id="super">
<button class="btn btn-danger" id="querybtn"> 查问 </button>
<button class="btn btn-success" id="exportbtn"> 导出 Excel</button>
<button class="btn btn-info" id="delbtn"> 删除 </button>
<button class="btn btn-info" id="checkbtn"> 审核 </button>
</div>
</div>
</div>
<!-- end 筛选 -->
</div>
<!-- listtable -->
<div>
<!-- gridview row -->
<table id="dg"></table>
<!-- end gridview row -->
</div>
<!--mainboxend-->
</div>
</div>
</div>
<!-- indexmain end -->
</div>
js 局部
<script>
$(document).ready(function() {
// 初始化页面
loaddg();
// 用户列表
LoadDDL('q_user','USER');
});
// 加载数据列表
function loaddg() {$('#dg').datagrid({
loadMsg: '正在查问,请稍后...',
title: '',
height: $(window).height() - 300,
url: '{:U(\'queryvehiclefixed\')}',
queryParams: {user: $('#q_user').combobox('getValue'),
cphm: $('#q_cphm').textbox('getValue'),
ksrq: $('#q_ksrq').datebox('getValue'),
jsrq: $('#q_jsrq').datebox('getValue')
},
nowrap: false,
striped: true,
collapsible: false,
loadMsg: '正在加载,请稍后。。。',
remoteSort: false,
singleSelect: true,
pageSize: 100,
idField: 'id',
pagination: true,
rownumbers: true,
pagination: true,
pageNumber: 1,
pageSize: 20,
pageList: [20, 40, 80, 160],
fitColumns: true,
columns: [
[{
field: 'cphm',
title: '车牌号码',
width: 50
}, {
field: 'date',
title: '申请工夫',
width: 70
}, {
field: 'user',
title: '申请人',
width: 70
}, {
field: 'part',
title: '所属部门',
width: 70
}, {
field: 'description',
title: '问题形容',
width: 100
}, {
field: 'mileage',
title: '公里数',
width: 50
}, {
field: 'zgsp',
title: '主管审批',
width: 50,
styler: function(value,row,index){if (value =='批准'){return 'color:green;';}
else if(value == '回绝'){return 'color:red;';}
}
}]
]
});
$("#querybtn").click(function() {$('#dg').datagrid('load', {"user": $('#q_user').combobox('getValue'),
"cphm": $('#q_cphm').textbox('getValue'),
"ksrq": $('#q_ksrq').datebox('getValue'),
"jsrq": $('#q_jsrq').datebox('getValue')
});
});
}
// 删除
$('#delbtn').click(function(){var row = $('#dg').datagrid('getSelected');
if(row){
layer.confirm('您确定要删除选中的数据?', {btn: ['是','否'] // 按钮
}, function(){
var option = {
type: "POST",
url: "{:U('delvehiclefixed')}",
data: {id:row.id},
success: function (data) {layer.closeAll();
layer.msg(data);
$('#dg').datagrid('reload');
}
};
$.ajax(option);
}, function(){layer.closeAll();
});
}
else{layer.msg('请抉择须要删除的数据!');
}
})
// 审核
$('#checkbtn').click(function(){var row = $('#dg').datagrid('getSelected');
if(row){
layer.confirm('请对此条申请做出审核', {btn: ['批准','不批准'] // 按钮
}, function(){
var option = {
type: "POST",
url: "{:U('checkvehiclefixed')}",
data: {id:row.id,ret:'02'},
success: function (data) {layer.closeAll();
layer.msg(data);
$('#dg').datagrid('reload');
}
};
$.ajax(option);
}, function(){
var option = {
type: "POST",
url: "{:U('checkvehiclefixed')}",
data: {id:row.id,ret:'03'},
success: function (data) {layer.closeAll();
layer.msg(data);
$('#dg').datagrid('reload');
}
};
$.ajax(option);
});
}
else{layer.msg('请抉择须要审核的数据!');
}
})
</script>