This project is mainly for internal employees of the enterprise. In addition to most of the functional modules commonly used in OA offices, there are also some customized functional modules. PHP + BootStrap + Easyui used in the background (PS: Does it feel like a long-term technology).
Features
1. Attendance check-in, overtime check-in
2. Office process application and approval
3. Notification delivery, SMS message reminder
4. Personal attendance record query, monthly statistics, drill query details
mind Mapping
Technical points
Flex layout, amap map application, message push, SMS reminder.
application module
project directory
Development introduction
Home Navigation
Using tabLayout on the home page of the system, you can configure the relevant parameters in the JSON file, and then set the value of content to the path of the JSON file in config.xml. If there are no special needs for bottom navigation, it is strongly recommended that you use tabLayout for the layout of the APP. The official has adapted various mobile phone screens and different resolutions, eliminating many problems related to adaptation.
{
"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"
}]
}
}
interface call
Two JS plugins, model.js and config.js, are encapsulated respectively for the interface call and interface configuration. In this way, unified management avoids repeatedly writing code when each page makes an interface call, which effectively simplifies the amount of code for each functional page. You only need to focus on writing your own business logic in the callback.
Plugin reference
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};
Call the interface in the page
//获取当前用户的本月考勤记录
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();
});
},
message push
The message push adopts the official push module, because the events that generate message reminders are all triggered in the APP, and all the official push modules are used; if there are background system operations that generate message reminders, the official push module is not applicable. , you need to use a third-party message push platform module such as Jpush, and cooperate with the background SDK to push messages.
User binding
//判断是否绑定推送
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'});
}
});
}
forward news
//发送抄送通知
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 layout
Flex layout is the top priority in AVM development! In the same sentence, the flex layout is well written, with CSS foundation, and there is no need to use UI components at all, and the UI design draft can be realized.
Regarding the flex layout, I recommend Mr. Ruan Yifeng's tutorial. After reading it several times and using it, it will naturally be handy! Link above: https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html
announcement
Since the content of the notification announcement is edited in the background through a rich text editor, there will be elements of style layout, and it is no longer a simple text display. Here, the rich-text component in AVM is used, and this component can be very good. Support some html element tags, which can perfectly display the content of rich text editing.
<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>
Data list and paging query
The display of the data list uses the scroll-view tag to refresh the data list and perform paging queries through onrefresherrefresh and onrefresherrefresh events. The refresher-triggered property is used to set the current pull-down refresh state, true means that the pull-down refresh has been triggered, and false means that the pull-down refresh has not been triggered. If you want to pull down to refresh by default, you can set it to true in apiready instead of performing data refresh operations.
If each item in the list has fewer elements, and there are no special requirements for styling, you can also use list-view to achieve this.
Below is the complete page code for the notification announcement list. The basic functions of the lists on other pages are the same, but there are differences in the style and number of parameters of each item.
<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>
component development
In this project, the default page of the module and the page without data are encapsulated as components, which is convenient for the page with data query, and the component can be directly referenced when there is no data. In the event project requirements, try to encapsulate common code modules into components, which not only simplifies the amount of page code, but also facilitates project maintenance. Once the content of the component is modified, it can be applied to many pages using the component.
For specific development tutorials, you can refer to the official tutorials and write them in combination with the tutorials in the official ordering templates. Here is the official link: https://docs.apicloud.com/APICloud/Order-template-description
It should be noted that installed is used in the component and apiready is used in the page. If apiready is used in the component, no error will be reported, but the result you want will not be executed.
map module usage
This application uses the map amap. The specific usage tutorial can be learned in detail through the module usage tutorial. The amp module contains very rich functions, which can basically meet 99% of the needs of the map.
The following mainly describes the pits that have been stepped on in the process of using the Gaode map:
1. Since the AutoNavi map is a native module, if the map in a page is only a part of the elements, you need to pay attention to the size and position of the map, because the native module will mask the page elements, so after fixing the position of the map element, Other elements in the page should also be adjusted. I used a blank view element to occupy the position of the map component, and then adjusted the elements of other pages.
2. Since the attendance check in this project is based on the check-in location to determine whether it is fieldwork, the isCircleContainsPoint method is just used, but it should be noted that this method is only valid after calling the open interface, because it is done at the beginning. In order to find address information based on latitude and longitude, the getNameFromCoords used does not need to call the open interface. The open interface was not called, which led to the use of the isCircleContainsPoint interface to be invalid all the time, and it was almost depressing!
3. The new version of AutoNavi map should be required by the Ministry of Industry and Information Technology. Since version 1.6.0 of this module, the privacy agreement must be popped up before calling this module for the first time. For details, please refer to the SDK compliance usage plan. After that, you need to call updateMapViewPrivacy, updateSearchPrivacy, otherwise the map and search interface will be invalid.
If your project used an old version of amap before, and it was upgraded to the latest version when it was packaged, be sure to add these two interfaces!
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:'加载地图失败'
})
}
});
Taking pictures and selecting pictures
Because the project attendance check-in requires each person to take 3 photos per day, and the current mobile phone has a high pixel, the photo size is too large, which seriously consumes the server memory; therefore, the FNPhotograph module is used to take pictures, and the open interface of its own UI allows you to choose to take pictures. The quality of the photos can be configured to use the camera direction, and the photos can be configured not to be stored in the album, and the display album button can be disabled to ensure that users can only take pictures on the spot, which can meet the needs of the project.
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:'图片预览失败'
})
}
});
},
Regarding the setting of the user's avatar, the user can choose to take a photo and select a photo from the album. At the same time, it supports cropping to meet the needs of user avatar settings. Cropping uses the FNImageClip module. When using the FNImageClip module, it is recommended to open a new frame page, perform the cropping operation on the new frame page, and update the avatar by pushing the event listener after the cropping is completed!
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>
Picture Preview
Many pages in the project involve the function of image preview, which is divided into single image preview and multi-image preview. The image preview uses the photoBrowser module.
photoBrowser is a picture browser that supports viewing of single and multiple pictures, zooming in and out of pictures, and supporting local and network picture resources. If the network image resources are cached locally, the resources cached locally can be manually cleared through the clearCache interface. At the same time, this module supports horizontal and vertical screen display. When the app supports horizontal and vertical screens, the bottom layer of this module will automatically monitor the position status of the current device and automatically adapt to the horizontal and vertical screens to display pictures. Use this module for developers to see a cool image browser.
<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();
}
});
}
clear cache
Since there are a lot of photos and photos in the project, a lot of caches will be generated in the process of use. Too much cache will cause the application to respond slowly. Therefore, the function of clear cache is added to the application, and the official api .clearCache is used.
In the personal center apiready, first get the cache in the application, and then click the clear cache button to clear it.
<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;
},
Registration page, send mobile phone verification code
The core code is how to set the countdown to start the verification code again and disable the click event after the verification code is sent successfully.
<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>
backend system
Login interface, registration interface, sending mobile phone verification code, list query interface, among which the mobile phone SMS uses Ali's SMS.
The SDK of Alibaba SMS is installed through composer, and it can be referenced in the header of the php file that needs to be called.
<?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();
}
}
}
References from the background system page about easyui and 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>
The grid layout of bootstrap is mainly used as the page layout.
eaysui uses version 1.5.3 and uses these controls in the following figure. For specific instructions, you can download a chm API manual.
html page
<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 part
<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('请选择需要删除的数据!');
}
})
//审核
$(
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。