使用 APICloud AVM 多端框架开发企业移动 OA 办公的项目实践
- 2022 年 5 月 16 日
本文字数:21406 字
阅读完需:约 70 分钟
本项目主要是针对企业内部员工使用,除了大部分 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>
版权声明: 本文为 InfoQ 作者【APICloud】的原创文章。
原文链接:【http://xie.infoq.cn/article/8321e7430611b7b75e2e46d82】。文章转载请联系作者。
APICloud
一次编码多端运行,移动应用低代码开发平台 2020.12.22 加入
用友YonBuilder移动端低代码开发平台,快速构建高性能多端应用
评论