页面效果展示

基于微信小程序实现精准定位,实现腾讯地图考勤打卡功能_定位打卡

基于微信小程序实现精准定位,实现腾讯地图考勤打卡功能_高德地图_02

集成腾讯地图SDK

腾讯位置服务为微信小程序提供了基础的标点能力、线和圆的绘制接口等地图组件和位置展示、地图选点等地图API位置服务能力支持,使得开发者可以自由地实现自己的微信小程序产品。 在此基础上,腾讯位置服务微信小程序JavaScript SDK是专为小程序开发者提供的LBS数据服务工具包,可以在小程序中调用腾讯位置服务的POI检索、关键词输入提示、地址解析、逆地址解析、行政区划和距离计算等数据服务,让您的小程序更强大!

文档地址:微信小程序JavaScript SDK

使用步骤说明:

1.申请开发者密钥(key):申请密钥

基于微信小程序实现精准定位,实现腾讯地图考勤打卡功能_考勤打卡_03

2.开通webserviceAPI服务

控制台 ->应用管理 -> 我的应用 ->添加key-> 勾选WebServiceAPI -> 保存

(小程序SDK需要用到webserviceAPI的部分服务,所以使用该功能的KEY需要具备相应的权限)

基于微信小程序实现精准定位,实现腾讯地图考勤打卡功能_高德地图_04

3.下载微信小程序JavaScriptSDK

微信小程序JavaScriptSDK v1.1 JavaScriptSDK v1.2 ,这里推荐下载1.2版本,将下载好的SDK放在对应文件夹中,去引用它(即 qqmap-wx-jssdk.min.js 文件)引用到你小程序项目中。

4.安全域名设置

在小程序管理后台 -> 开发 -> 开发管理 -> 开发设置 -> “服务器域名” 中设置request合法域名,添加https://apis.map.qq.com

这个操作需要小程序管理员进到后台去配置 本地环境开发只需设置 勾上 不校验合法域名 即可

基于微信小程序实现精准定位,实现腾讯地图考勤打卡功能_定位打卡_05

5.小程序核心代码示例

// 引入SDK核心类,js文件根据自己业务,位置可自行放置 var QQMapWX = require('../../libs/qqmap-wx-jssdk.js'); var qqmapsdk; Page({

onLoad: function () {
    // 实例化API核心类
    qqmapsdk = new QQMapWX({
        key: '申请的key'
    });
},
onShow: function () {
    // 调用接口
    qqmapsdk.search({
        keyword: 'DreamCoders',
        success: function (res) {
            console.log(res);
        },
        fail: function (res) {
            console.log(res);
        },
    complete: function (res) {
        console.log(res);
    }
 });
    

微信小程序代码 wx.getLocation(Object object)

以 Promise 风格 调用:支持

用户授权:需要 scope.userLocation

小程序插件:支持,需要小程序基础库版本不低于 1.9.6

微信 Windows 版:支持

微信 Mac 版:支持

功能描述 获取当前的地理位置、速度。当用户离开小程序后,此接口无法调用。开启高精度定位,接口耗时会增加,可指定 highAccuracyExpireTime 作为超时时间。地图相关使用的坐标格式应为 gcj02。 高频率调用会导致耗电,如有需要可使用持续定位接口 wx.onLocationChange。 基础库 2.17.0 版本起 wx.getLocation 增加调用频率限制,相关公告。

使用方法 自 2022 年 7 月 14 日后发布的小程序,若使用该接口,需要在 app.json 中进行声明,否则将无法正常使用该接口,2022年7月14日前发布的小程序不受影响。具体规则见公告

申请开通 暂只针对如下类目的小程序开放,需要先通过类目审核,再在小程序管理后台,「开发」-「开发管理」-「接口设置」中自助开通该接口权限。 接口权限申请入口将于2022年3月11日开始内测,于3月31日全量上线。并从4月18日开始,在代码审核环节将检测该接口是否已完成开通,如未开通,将在代码提审环节进行拦截。

微信小程序获取定位关键方法 getLocation。因此需要在 app.json 中进行声明,后期小程序上线还需要单独申请getLocation 接口权限。

app.json 部分关键代码

{
  "pages": [
    "pages/index/index",
    "pages/sign/sign"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle": "black"
  },
  "tabBar": {
		"custom": false,
		"backgroundColor": "#fefefe",
		"color": "#999999",
		"selectedColor": "#1C9D9D",
		"list": [{
				"pagePath": "pages/index/index",
				"text": "首页",
				"iconPath": "/images/home.png",
				"selectedIconPath": "/images/home_cur.png"
			},
			{
				"pagePath": "pages/sign/sign",
				"text": "打卡",
				"iconPath": "/images/day.png",
				"selectedIconPath": "/images/day_cur.png"
			},
		
			{
        "pagePath": "pages/index/index",
				"text": "我的",
				"iconPath": "/images/my.png",
				"selectedIconPath": "/images/my_cur.png"
			}
		]
	},
  "permission": {
		"scope.userLocation": {
			"desc": "您的位置信息将用于小程序考勤签到功能"
		}
	},
	"requiredPrivateInfos":[
		"getLocation"
	],
  "style": "v2",
  "sitemapLocation": "sitemap.json",
  "lazyCodeLoading": "requiredComponents"
}

sign.wxml代码

<!-- 部分UI代码参考苏苏就是小苏苏 -->
<view class="index">
  <!-- 用户信息 -->
  <view class="head ">
    <view class="head_box flex-row" style="justify-content:left">
      <view class="user_ava">
        <open-data type="userAvatarUrl"></open-data>
      </view>
      <view>
        <view class="user_name">DreamCoders <text>{{tip}}</text></view>
        <view class="user_add">新的一天开始了,加油哦~</view>
      </view>
      <view class="mealBtn" bindtap="ToMealTap">
        <image src="/images/meal.png"></image>
        <view class="mealText">{{is_meal==2 ? '已订':'订餐'}}</view>
      </view>
    </view>
  </view>
 
  <view class="contentBox">
    <!-- 打卡记录 -->
    <view class="signRecord">
      <view class="signInfo">上班打卡
        <text class="text-green">{{record[0].times ? record[0].times : '未打卡'}}</text>
        <view class="sign_address">
          <view class="">{{record[0].address ? record[0].address : '暂无打卡地址'}}</view>
        </view>
      </view>
      <view class="signInfo">下班打卡
        <text class="text-green">{{record[1].times ? record[1].times : '未打卡'}}</text>
        <view class="sign_address">
          <view class="">{{record[1].address ? record[1].address : '暂无打卡地址'}}</view>
        </view>
      </view>
    </view>
    <view class="dateInfo ">
      <text>{{nowDate}} {{nowDay}}</text>
    </view>
    <!-- 打卡按钮 -->
    <view class="c_clock flex-column">
      <view class="clock_time flex-column j_c {{status==1?'c1':''}} {{is_out==2 ? 'outArea' : ''}}" catchtap="signTap">
        <text>{{signType>0 ? "下班打卡" : "上班打卡"}}</text>
        <text>{{now_time}}</text>
      </view>
      <view class="clock_time_over flex-column j_c  {{status==1?'c2':''}}" catchtap="clockInStart">
        <text>已打卡</text>
        <text>{{now_time_stop}}</text>
      </view>
    </view>
    <!-- 打卡地址 -->
    <view class="clock_address ">
      <image src="/images/add0.png" class="add_icon" />
      <text>{{current_address}}</text>
    </view>
    <view class="refresh" catchtap="refreshAdd">刷新位置</view>
  </view>
</view>

sign.js代码

具体业务逻辑根据实际情况改写

let qqMapSdk= require("../../utils/qqmap.js");
let util = require('../../utils/util.js')
 
Page({
  /**
   * 页面的初始数据
   */
  data: {
    signType:0,//0上班打卡 1下班打卡
    is_out:2,//1办公地点打卡 2外勤打卡
    is_meal:1,//1未定餐 2已订餐
    now_time: '',//当前时间
    nowDate:'',//当前年月日
    nowDay:'',//星期几
    tip:'',//提示 上午好、下午好
    current_address: '',//当前定位地址
    status: 0, //0未打卡 1已打卡
    latlng:[],//经纬度
    now_time_stop: '', //已打卡时间
    area:{},//考勤点多个
    record:[],//打卡记录
  },
 
 
  onLoad: function (options) {
    this.getCurrentTime();
    this.setData({
      now_time: this.getTime(),
      nowDate: util.formatTime(new Date()),
      nowDay: util.formatDay(new Date()),
      tip: util.formatSole(),
    })
  },
 
  onShow: function () {
    this.getLocation();
    this.setData({
      status:0,
      current_address:'',
    })
  },
 
 
  signTap() {
    var that = this;
    if (!that.data.current_address) {
      return wx.showToast({
        title: '未获取当前定位',
        icon: 'error'
      })
    }
    var list =  that.data.record.concat({'times':that.data.now_time,'address':that.data.current_address});
    wx.vibrateLong();//手机震动提示
    that.getSignRecord();
    that.setData({
      status: 1, //已打卡
      record:list,
      now_time_stop: that.data.now_time,
    })
    console.log(list);
    console.log(that.data.record);
    wx.showToast({
      title: '打卡成功',
      icon: 'none'
    })
  },
 
  getCurrentTime: function () {
    var time = setInterval(() => {
      this.setData({
        now_time: this.getTime()
      })
    }, 1000)
  },
  getTime() {
    let dateTime = '';
    let hh = new Date().getHours()
    let mf = new Date().getMinutes() < 10 ? '0' + new Date().getMinutes() :
      new Date().getMinutes()
    let ss = new Date().getSeconds() < 10 ? '0' + new Date().getSeconds() :
      new Date().getSeconds()
    dateTime = hh + ':' + mf + ':' + ss;
    return dateTime;
  },
 
  // 请求获取定位授权
  getUserAuth: function () {
    return new Promise((resolve, reject) => {
      wx.authorize({
        scope: 'scope.userLocation'
      }).then(() => {
        resolve()
      }).catch(() => {
        let that = this;
        wx.getSetting({
          success: (res) => {
            if (res.authSetting['scope.userLocation'] != undefined && res.authSetting['scope.userLocation'] != true) {
              wx.showModal({
                title: '请求授权当前位置',
                content: '需要获取您的地理位置,请确认授权',
                success: function (res) {
                  if (res.cancel) {
                    wx.showToast({
                      title: '拒绝授权',
                      icon: 'none',
                      duration: 1000
                    })
                  } else if (res.confirm) {
                    wx.openSetting({
                      success: function (dataAu) {
                        if (dataAu.authSetting["scope.userLocation"] == true) {
                          //再次授权,调用wx.getLocation的API
                          that.getLocation();
                        } else {
                          wx.showToast({
                            title: '授权失败',
                            icon: 'none',
                            duration: 1000
                          })
                        }
                      }
                    })
                  }
                }
              })
            } else if (res.authSetting['scope.userLocation'] == undefined) {
              that.getLocation();
            } else {
              that.getLocation();
            }
          }
        })
      })
    })
  },
 
  getLocation: function () {
    const that = this
		// 实例化腾讯地图API核心类
    const QQMapWX = new qqMapSdk({
      key: '你申请的KEY'// KEY必填
    });
    //获取当前位置
    wx.getLocation({
      type: 'gcj02',
      success: function(res) {
        that.latitude = res.latitude
        that.longitude = res.longitude
        QQMapWX.reverseGeocoder({
          location: {
            latitude: res.latitude,
            longitude: res.longitude
          },
          success: function(res) {
            let address = res.result.address + res.result.formatted_addresses.recommend;
            that.getSignRecord();
            that.setData({
              current_address:address,
              latlng:[res.result.location.lat,res.result.location.lng]
            })
          },
          fail: function(res) {
            this.getUserAuth()
            wx.showToast({
              title: '获取定位失败,请打开定位,重新进入!',
              icon: 'none'
            });
          }
        })
      },
    })
  },
  // 刷新定位
  refreshAdd() {
    this.getLocation(),
    this.getSignRecord()
  },
 
  //处理打卡记录及判断打卡位置是否办公地点打卡
  getSignRecord: function () {
    var that = this;
    console.log(that.data.latlng);
    var distance = that.getDistance(that.data.latlng[0],that.data.latlng[1],31.370450,121.228252);
    if(distance < 200000000000000){
      that.setData({
        is_out:1,
      })
    }
  },
 
 
//经纬度距离计算
getDistance:function (lat1, lng1, lat2, lng2, unit = false) {
    var radLat1 = lat1 * Math.PI / 180.0
    var radLat2 = lat2 * Math.PI / 180.0
    var a = radLat1 - radLat2
    var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0
    var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
      Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)))
    s = s * 6378.137 // EARTH_RADIUS;
    s = Math.round(s * 10000) / 10000 //输出为公里
    if (0) { //是否返回带单位
      if (s < 1) { //如果距离小于1km返回m
        s = s.toFixed(3)
        s = s * 1000 + "m"
      } else {
        s = s.toFixed(2)
        s = s + "km"
      }
    } else {
      s = s.toFixed(3)
      s = s * 1000
    }
	return s
},
 
//订餐操作
ToMealTap:function (e) {
  wx.showToast({
    title: '订餐成功',
    icon: 'none'
  })
  this.setData({
    is_meal: 2,
  })
}
 
})

sign.wxss 代码

 
page {
  height: calc(100% - 10px)
}
 
.index {
  margin-top: 10px;
  background: #fff;
  min-height: 100%;
}
 
.head {
  padding-bottom: 10rpx;
  border-bottom: 2rpx solid #E5E5E5;
}
 
.head_box {
  padding: 26rpx 28rpx 8px;
  width: 750rpx;
  box-sizing: border-box;
}
 
.user_ava {
  width: 116rpx;
  height: 116rpx;
  overflow: hidden;
  border-radius: 25%;
  margin-right: 32rpx;
}
 
.user_name {
  font-size: 32rpx;
  font-weight: 600;
  color: #333333;
  margin-bottom: 18rpx;
}
 
.user_name text {
  font-size: 24rpx;
  color: #999999;
  font-weight: 400;
  margin-left: 40rpx;
}
 
.user_add {
  font-size: 28rpx;
  color: #3380F3;
}
 
.contentBox {
  padding: 44rpx 28rpx;
}
 
.signRecord{
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
  margin-top: 15px;
}
 
.dateInfo{
  text-align: center;
  position: relative;
  top: 50px;
  font-size: 35rpx;
}
 
.c_title {
  font-size: 28rpx;
  color: #666666;
  margin-bottom: 26rpx;
}
 
 
.c_section .c_item {
  position: relative;
  font-size: 30rpx;
  font-weight: 600;
  color: #333333;
  padding-left: 40rpx;
  margin-bottom: 110rpx;
}
 
.c_section text {
  color: #307CED;
  text-overflow: ellipsis;
  overflow: hidden;
  width: 80%;
  white-space: nowrap;
}
 
.c_section .c_item::before {
  content: '';
  position: absolute;
  width: 18rpx;
  height: 18rpx;
  border: 2rpx solid #999999;
  left: 0;
  top: 50%;
  margin-top: -9rpx;
  border-radius: 50%;
}
 
.c_section {
  position: relative;
}
 
.c_section .c_item::after {
  content: '';
  position: absolute;
  width: 2rpx;
  height: 178rpx;
  background: #E6E6E6;
  left: 10rpx;
  top: 34rpx;
}
 
.c_section view:last-child::after {
  display: none;
}
 
.start_lo {
  position: absolute;
  top: 30px;
  left: -5px;
}
 
.start_end {
  position: absolute;
  bottom: -108px;
  left: 20px;
}
 
.c_clock {
  margin: 180rpx auto 0;
  width: 350rpx;
  height: 380rpx;
  perspective: 1500;
  -webkit-perspective: 1500;
  -moz-perspective: 1500;
}
 
.clock_time {
  width: 350rpx;
  height: 350rpx;
  margin-bottom: 30rpx;
  position: absolute;
  transition: all 1s;
  backface-visibility: hidden;
}
 
.clock_time::after {
  content: '';
  top: 0;
  left: 0;
  width: 350rpx;
  height: 350rpx;
  border-radius: 50%;
  position: absolute;
  z-index: 9;
  background: rgba(48, 124, 237, 0.08);
  animation: scale 1s infinite alternate-reverse;
}
 
/* 已打卡 */
.clock_time_over {
  width: 350rpx;
  height: 350rpx;
  margin-bottom: 30rpx;
  border-radius: 50%;
  background: rgba(48, 124, 237, 0.08);
  position: absolute;
  transition: all 1s;
  backface-visibility: hidden;
  transform: rotateY(-180deg);
}
 
.clock_time_over::after {
  position: absolute;
  z-index: 11;
  content: '';
  width: 320rpx;
  height: 320rpx;
  background: #C6CED9;
  border-radius: 50%;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
 
 
 
.clock_time_over text {
  position: relative;
  z-index: 13;
  color: #FFFFFF;
}
 
.clock_time_over text:first-child {
  font-size: 36rpx;
  margin-bottom: 14rpx;
}
 
.clock_time_over text:last-child {
  font-size: 28rpx;
}
 
@keyframes scale {
 
  0% {
    transform: scale(1.1);
  }
 
  100% {
    transform: scale(1);
  }
}
 
.clock_time::before {
  position: absolute;
  z-index: 11;
  content: '';
  width: 320rpx;
  height: 320rpx;
  background: rgb(48, 124, 237, 0.79);
  border-radius: 50%;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
 
.clock_time text {
  position: relative;
  z-index: 13;
  color: #FFFFFF;
}
 
.clock_time text:first-child {
  font-size: 36rpx;
  margin-bottom: 14rpx;
}
 
.clock_time text:last-child {
  font-size: 45rpx;
}
 
.clock_address {
  text-align: center;
  font-size: 30rpx;
  color: #333333;
  width: 80%;
  margin: 20px auto;
  overflow:hidden;  
  text-overflow:ellipsis;
  white-space:nowrap;
}
 
.clock_address text {
  vertical-align: middle;
}
 
.add_icon {
  width: 28rpx;
  height: 36rpx;
  margin-right: 16rpx;
  vertical-align: middle;
}
 
.refresh {
  margin-top: 25px;
  color: #307CED;
  display: flex;
  align-items: center;
  justify-content: center;
}
 
 
 
.now_location {
  font-size: 24rpx;
  color: #333333 !important;
}
 
.upload_box {
  width: 260rpx;
  height: 180rpx;
  background: #F5F5F8;
  border-radius: 5rpx;
}
 
.upload_box text {
  font-size: 20rpx;
  color: #999 !important;
  font-weight: 100;
}
 
.camera_icon {
  width: 42rpx;
  height: 44rpx;
  margin-bottom: 10rpx;
}
 
.clock_img {
  width: 100%;
  height: 100%;
}
 
.del_icon {
  width: 32rpx;
  height: 32rpx;
  position: absolute;
  right: -4px;
  top: -11rpx;
}
 
.ative::before {
  background: #307cedc9;
  border: 2rpx solid #307cedc9 !important;
}
 
.c1 {
  transform: rotateY(180deg)
}
 
.c1::after {
  animation: none !important;
}
 
.c2 {
  transform: rotateY(0deg)
}
 
.mealBtn{
  position: absolute;
  right: 15px;
}
.mealBtn image{
  width: 27px;
  height: 27px;
}
.mealText{
 font-size: 12px;
 color: #999999;
}
 
.outArea::before{
  background: #f44336 !important;
}
.signInfo{
  width: 48%;
  height: 65px;
  background: #f1f1f1;
  padding: 10px;
  border-radius: 5px;
}
.signInfo text{
  float: inline-end;
}
.sign_address{
  display: flex;
  margin-top: 5px;
}
 
 
.sign_address view{
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  font-size: 14px;
  margin-top: 1px !important;
  color: #5f5a5a;
}
 
.text-green{
  color: green;
}

util.js 代码

function formatTime(date) {
  var year = date.getFullYear()
  var month = date.getMonth() + 1
  var day = date.getDate()
  return year + "年" + month + "月" + day + "日";
}
const formatDay = dates => {
  let _day = new Array('星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六');
  let date = new Date(dates);
  date.setDate(date.getDate());
  let day = date.getDay();
  return _day[day];
}
const formatSole = () => {
  let timeNow = new Date();
  let hours = timeNow.getHours();
  let text = ``;
  if (hours >= 0 && hours <= 6) {
    text = `深夜了,不要熬夜太久哟`;
  } else if (hours > 6 && hours <= 8) {
    text = `早上好`;
  } else if (hours > 8 && hours <= 10) {
    text = `上午好`;
  } else if (hours > 10 && hours <= 13) {
    text = `中午好`;
  } else if (hours > 13 && hours <= 17) {
    text = `下午好`;
  } else if (hours > 17 && hours <= 23) {
    text = `晚上好`;
  }
  return text;
}
module.exports = {
  formatTime: formatTime,
  formatDay: formatDay,
  formatSole: formatSole
}

源码地址:

https://gitee.com/iGaoWei/miniWxDemo