|
|
|
|
@ -0,0 +1,784 @@
|
|
|
|
|
<template>
|
|
|
|
|
<view class="checkin-page">
|
|
|
|
|
<view class="checkin-container">
|
|
|
|
|
<!-- 课程信息卡片 -->
|
|
|
|
|
<view class="course-card">
|
|
|
|
|
<view class="course-title">{{ course.theme?course.theme:'' }}</view>
|
|
|
|
|
<view class="course-info">
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
<u-icon name="calendar-fill" class="info-icon"></u-icon>
|
|
|
|
|
<text>{{ course.date?course.date:'' }} - {{ course.period?course.period:'' }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
<u-icon name="map-fill" class="info-icon"></u-icon>
|
|
|
|
|
<text>{{ course.address?course.address:'' }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
<u-icon name="account-fill" class="info-icon"></u-icon>
|
|
|
|
|
<text>{{ course.teacher?course.teacher.name:'' }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
<!-- 当前其他可签到课程 -->
|
|
|
|
|
<view class="status-card" v-if="courseContentList.length > 0">
|
|
|
|
|
<h6 class="card-title">
|
|
|
|
|
<u-icon name="order" class="title-icon"></u-icon>
|
|
|
|
|
当前其他可签到课程
|
|
|
|
|
</h6>
|
|
|
|
|
<view v-for="item in courseContentList" :key="item.id" class="course-card">
|
|
|
|
|
<view class="course-title">{{ item.theme || '' }}</view>
|
|
|
|
|
<view class="course-info">
|
|
|
|
|
<view class="info-item" v-if="item.date || item.period">
|
|
|
|
|
<u-icon name="calendar-fill" class="info-icon"></u-icon>
|
|
|
|
|
<text>{{ item.date || '' }} - {{ item.period || '' }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="info-item" v-if="item.address">
|
|
|
|
|
<u-icon name="map-fill" class="info-icon"></u-icon>
|
|
|
|
|
<text>{{ item.address }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="info-item" v-if="item.teacher && item.teacher.name">
|
|
|
|
|
<u-icon name="account-fill" class="info-icon"></u-icon>
|
|
|
|
|
<text>{{ item.teacher.name }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
<!-- 状态检测卡片 -->
|
|
|
|
|
<view class="status-card">
|
|
|
|
|
<h6 class="card-title">
|
|
|
|
|
<u-icon name="shield-checkmark" class="title-icon"></u-icon>
|
|
|
|
|
签到状态检测
|
|
|
|
|
</h6>
|
|
|
|
|
<view class="status-item">
|
|
|
|
|
<text class="status-label">定位状态</text>
|
|
|
|
|
<view :class="['status-value', 'status-' + locationStatus.type]">
|
|
|
|
|
<u-icon :name="getStatusIcon(locationStatus.type)"></u-icon>
|
|
|
|
|
<text>{{ locationStatus.text }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
<view class="status-item">
|
|
|
|
|
<text class="status-label">打卡范围</text>
|
|
|
|
|
<view :class="['status-value', 'status-' + rangeStatus.type]">
|
|
|
|
|
<u-icon :name="getStatusIcon(rangeStatus.type)"></u-icon>
|
|
|
|
|
<text>{{ rangeStatus.text }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 距离信息 -->
|
|
|
|
|
<view v-if="distance !== null" class="distance-info">
|
|
|
|
|
<view class="distance-value">{{ formattedDistance }}</view>
|
|
|
|
|
<view class="distance-label">距离课程地点</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 提示信息 -->
|
|
|
|
|
<view v-if="alertInfo.message" :class="['alert-custom', 'alert-' + alertInfo.type]">
|
|
|
|
|
<u-icon :name="getStatusIcon(alertInfo.type)" class="alert-icon"></u-icon>
|
|
|
|
|
<text>{{ alertInfo.message }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 签到操作 -->
|
|
|
|
|
<view class="checkin-actions">
|
|
|
|
|
<u-button
|
|
|
|
|
type="primary"
|
|
|
|
|
class="checkin-btn"
|
|
|
|
|
:disabled="!canCheckin"
|
|
|
|
|
@click="performCheckin"
|
|
|
|
|
>
|
|
|
|
|
<u-icon name="map" class="btn-icon"></u-icon>
|
|
|
|
|
{{ hasCheckedIn ? '已签到' : '位置签到' }}
|
|
|
|
|
</u-button>
|
|
|
|
|
<u-button
|
|
|
|
|
type="success"
|
|
|
|
|
class="recheck-btn"
|
|
|
|
|
@click="handleRecheckClick"
|
|
|
|
|
>
|
|
|
|
|
<u-icon :name="hasLocationPermission ? 'reload' : 'location'" class="btn-icon"></u-icon>
|
|
|
|
|
{{ hasLocationPermission ? '重新定位' : '获取定位权限' }}
|
|
|
|
|
</u-button>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 签到记录 -->
|
|
|
|
|
<view class="status-card">
|
|
|
|
|
<h6 class="card-title">
|
|
|
|
|
<u-icon name="order" class="title-icon"></u-icon>
|
|
|
|
|
签到记录
|
|
|
|
|
</h6>
|
|
|
|
|
<view v-if="checkinHistory.length === 0" class="history-empty">
|
|
|
|
|
暂无签到记录
|
|
|
|
|
</view>
|
|
|
|
|
<view v-else class="history-list">
|
|
|
|
|
<view v-for="(record, index) in checkinHistory" :key="index" class="history-item">
|
|
|
|
|
<text>签到时间: {{ record.created_at }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
export default {
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
courseContentList:[],
|
|
|
|
|
course_content_id:'',
|
|
|
|
|
course: {},
|
|
|
|
|
userLocation: null,
|
|
|
|
|
distance: null,
|
|
|
|
|
canCheckin: false,
|
|
|
|
|
hasCheckedIn: false,
|
|
|
|
|
locationStatus: { type: 'default', text: '未定位' },
|
|
|
|
|
rangeStatus: { type: 'default', text: '未计算' },
|
|
|
|
|
alertInfo: { type: '', message: '' },
|
|
|
|
|
checkinHistory: [],
|
|
|
|
|
hasLocationPermission: false,
|
|
|
|
|
// 坐标系统配置
|
|
|
|
|
coordinateConfig: {
|
|
|
|
|
// 后端返回的坐标系统:'wgs84' 或 'gcj02'
|
|
|
|
|
backendSystem: 'wgs84',
|
|
|
|
|
// 是否启用坐标转换
|
|
|
|
|
enableConversion: true
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
formattedDistance() {
|
|
|
|
|
const d = this.distance
|
|
|
|
|
if (d == null) return ''
|
|
|
|
|
// >= 1km 显示为整 km;< 1km 显示米
|
|
|
|
|
if (d >= 1000) return `${Math.round(d / 1000)}km`
|
|
|
|
|
return `${d}m`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onLoad(options) {
|
|
|
|
|
console.log("页面加载,options:", options)
|
|
|
|
|
this.course_content_id = options?.course_content_id
|
|
|
|
|
|
|
|
|
|
// 测试坐标转换功能
|
|
|
|
|
this.testCoordinateConversion()
|
|
|
|
|
|
|
|
|
|
// 进入页面:先检查定位权限
|
|
|
|
|
console.log("开始检查权限...")
|
|
|
|
|
this.checkPermissionAndInit();
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
//获取同一天的课程列表
|
|
|
|
|
async getCourseList() {
|
|
|
|
|
const res = await this.$u.api.myCourseContent({
|
|
|
|
|
date: this.course.date
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 检查返回的数据是否为空
|
|
|
|
|
if (!res.list || res.list.length === 0) {
|
|
|
|
|
// 如果没有课程数据,禁用签到按钮
|
|
|
|
|
this.canCheckin = false;
|
|
|
|
|
this.courseContentList = [];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.courseContentList = res.list.filter(item => item.id != this.course.id)
|
|
|
|
|
},
|
|
|
|
|
async checkPermissionAndInit() {
|
|
|
|
|
console.log('开始检查权限...')
|
|
|
|
|
let granted = false
|
|
|
|
|
try {
|
|
|
|
|
const setting = await new Promise((resolve) => {
|
|
|
|
|
uni.getSetting({ success: resolve, fail: () => resolve({}) })
|
|
|
|
|
})
|
|
|
|
|
console.log('获取设置结果:', setting)
|
|
|
|
|
const auth = setting?.authSetting || {}
|
|
|
|
|
console.log('权限设置:', auth)
|
|
|
|
|
granted = !!auth['scope.userLocation']
|
|
|
|
|
this.hasLocationPermission = granted
|
|
|
|
|
console.log('权限检测结果:', granted, 'hasLocationPermission:', this.hasLocationPermission)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log('权限检测异常:', e)
|
|
|
|
|
this.hasLocationPermission = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 无论权限状态如何,都先获取课程信息
|
|
|
|
|
console.log('开始获取课程信息...')
|
|
|
|
|
await this.getCourse()
|
|
|
|
|
await this.signGet()
|
|
|
|
|
|
|
|
|
|
if (!granted) {
|
|
|
|
|
console.log('无权限,设置状态为未获取定位权限')
|
|
|
|
|
this.locationStatus = { type: 'warning', text: '未获取定位权限' }
|
|
|
|
|
this.rangeStatus = { type: 'default', text: '未计算' }
|
|
|
|
|
this.canCheckin = false
|
|
|
|
|
// 即使没有权限,也显示课程信息,但不进行定位相关操作
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 有权限则继续完整的签到流程
|
|
|
|
|
console.log('有权限,开始完整签到流程')
|
|
|
|
|
await this.initializeCheckin()
|
|
|
|
|
},
|
|
|
|
|
handleRecheckClick() {
|
|
|
|
|
console.log('按钮被点击,当前权限状态:', this.hasLocationPermission)
|
|
|
|
|
if (this.hasLocationPermission) {
|
|
|
|
|
console.log('有权限,执行重新定位')
|
|
|
|
|
this.initializeCheckin()
|
|
|
|
|
} else {
|
|
|
|
|
console.log('无权限,请求定位权限')
|
|
|
|
|
this.requestLocationPermission()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
requestLocationPermission() {
|
|
|
|
|
console.log('开始请求定位权限...')
|
|
|
|
|
uni.authorize({
|
|
|
|
|
scope: 'scope.userLocation',
|
|
|
|
|
success: () => {
|
|
|
|
|
console.log('定位权限授权成功')
|
|
|
|
|
this.hasLocationPermission = true
|
|
|
|
|
this.locationStatus = { type: 'warning', text: '定位中...' }
|
|
|
|
|
this.initializeCheckin()
|
|
|
|
|
},
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.log('定位权限授权失败:', err)
|
|
|
|
|
uni.openSetting({
|
|
|
|
|
success: (res) => {
|
|
|
|
|
const ok = res?.authSetting && res.authSetting['scope.userLocation']
|
|
|
|
|
if (ok) {
|
|
|
|
|
console.log('通过设置页面获取定位权限成功')
|
|
|
|
|
this.hasLocationPermission = true
|
|
|
|
|
this.locationStatus = { type: 'warning', text: '定位中...' }
|
|
|
|
|
this.initializeCheckin()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 获取当前位置经纬度(Promise 形式)
|
|
|
|
|
// 本地调试:从三个坐标中随机返回;接入真机定位时,改回 uni.getLocation
|
|
|
|
|
getLocation() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
// const candidates = [
|
|
|
|
|
// { lat: 31.401992, lng: 120.686876 },
|
|
|
|
|
// { lat: 32.899321, lng: 120.682145 },
|
|
|
|
|
// { lat: 31.272715, lng: 120.670337 }
|
|
|
|
|
// ]
|
|
|
|
|
// const idx = Math.floor(Math.random() * candidates.length)
|
|
|
|
|
// resolve(candidates[idx])
|
|
|
|
|
// 真实定位:
|
|
|
|
|
uni.getLocation({
|
|
|
|
|
// type: 'gcj02',
|
|
|
|
|
success: (res) => resolve({ lat: res.latitude, lng: res.longitude }),
|
|
|
|
|
fail: (err) => reject(err)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
// 获取课表信息
|
|
|
|
|
async getCourse() {
|
|
|
|
|
const res = await this.$u.api.courseContentDetail({
|
|
|
|
|
course_content_id: this.course_content_id
|
|
|
|
|
});
|
|
|
|
|
const data = res || {}
|
|
|
|
|
// 兼容后端字段:latitude/longitude 或 lat/lng
|
|
|
|
|
let lat = data.latitude || data.lat
|
|
|
|
|
let lng = data.longitude || data.lng
|
|
|
|
|
|
|
|
|
|
// 根据配置进行坐标转换
|
|
|
|
|
let convertedLng = lng
|
|
|
|
|
let convertedLat = lat
|
|
|
|
|
|
|
|
|
|
if (this.coordinateConfig.enableConversion && this.coordinateConfig.backendSystem !== 'gcj02') {
|
|
|
|
|
const [converted] = this.convertCoordinates(lng, lat, this.coordinateConfig.backendSystem, 'gcj02')
|
|
|
|
|
convertedLng = converted[0]
|
|
|
|
|
convertedLat = converted[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('原始坐标:', { lng, lat, system: this.coordinateConfig.backendSystem })
|
|
|
|
|
console.log('转换后坐标:', { lng: convertedLng, lat: convertedLat, system: 'gcj02' })
|
|
|
|
|
|
|
|
|
|
this.course = {
|
|
|
|
|
...this.course,
|
|
|
|
|
...data,
|
|
|
|
|
location: {
|
|
|
|
|
lat: convertedLat,
|
|
|
|
|
lng: convertedLng,
|
|
|
|
|
originalLat: lat,
|
|
|
|
|
originalLng: lng,
|
|
|
|
|
coordinateSystem: 'gcj02'
|
|
|
|
|
},
|
|
|
|
|
allowedDistance: 500
|
|
|
|
|
}
|
|
|
|
|
this.getCourseList()
|
|
|
|
|
},
|
|
|
|
|
// 获取签到记录
|
|
|
|
|
async signGet() {
|
|
|
|
|
const res = await this.$u.api.signGet({
|
|
|
|
|
course_content_id: this.course_content_id
|
|
|
|
|
});
|
|
|
|
|
const rows = res?.list || []
|
|
|
|
|
this.checkinHistory = Array.isArray(rows) ? rows : []
|
|
|
|
|
if (this.checkinHistory.length > 0) {
|
|
|
|
|
this.hasCheckedIn = true
|
|
|
|
|
this.canCheckin = false
|
|
|
|
|
this.alertInfo = { type: 'warning', message: '已存在签到记录,不能重复打卡' }
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 获取距离并更新状态(合并 signDistance + calculateDistance )
|
|
|
|
|
async signDistance() {
|
|
|
|
|
let distance = null
|
|
|
|
|
let allowed = this.course.allowedDistance || 500
|
|
|
|
|
try {
|
|
|
|
|
// 用户位置是GCJ-02坐标系(uni.getLocation返回的)
|
|
|
|
|
const userLat = this.userLocation.lat
|
|
|
|
|
const userLng = this.userLocation.lng
|
|
|
|
|
|
|
|
|
|
console.log('用户位置坐标:', { lat: userLat, lng: userLng, system: 'gcj02' })
|
|
|
|
|
console.log('课程位置坐标:', {
|
|
|
|
|
lat: this.course.location.lat,
|
|
|
|
|
lng: this.course.location.lng,
|
|
|
|
|
system: this.course.location.coordinateSystem
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const res = await this.$u.api.signDistance({
|
|
|
|
|
course_content_id: this.course_content_id,
|
|
|
|
|
latitude: userLat,
|
|
|
|
|
longitude: userLng
|
|
|
|
|
})
|
|
|
|
|
const data = res || {}
|
|
|
|
|
// 接口返回单位为 km,这里转为 m
|
|
|
|
|
if (data && data.distance != null && !isNaN(parseFloat(data.distance))) {
|
|
|
|
|
const km = parseFloat(data.distance)
|
|
|
|
|
distance = Math.round(km * 1000)
|
|
|
|
|
}
|
|
|
|
|
if (data && data.content_check_range != null && !isNaN(parseFloat(data.content_check_range))) {
|
|
|
|
|
const kmRange = parseFloat(data.content_check_range)
|
|
|
|
|
allowed = Math.round(kmRange * 1000)
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
if (distance === null) {
|
|
|
|
|
const distLocal = this.getDistanceFromLatLonInM(
|
|
|
|
|
this.userLocation.lat, this.userLocation.lng,
|
|
|
|
|
this.course.location.lat, this.course.location.lng
|
|
|
|
|
)
|
|
|
|
|
distance = Math.round(distLocal)
|
|
|
|
|
}
|
|
|
|
|
this.distance = distance
|
|
|
|
|
this.course.allowedDistance = allowed
|
|
|
|
|
|
|
|
|
|
if (this.distance <= allowed) {
|
|
|
|
|
this.rangeStatus = { type: 'success', text: '在打卡范围内' };
|
|
|
|
|
this.canCheckin = !this.hasCheckedIn;
|
|
|
|
|
this.alertInfo = this.hasCheckedIn
|
|
|
|
|
? { type: 'warning', message: '已存在签到记录,不能重复打卡' }
|
|
|
|
|
: { type: 'success', message: `您已进入打卡范围,距离${this.formatDistanceVal(this.distance)}。` };
|
|
|
|
|
} else {
|
|
|
|
|
const over = Math.max(0, this.distance - allowed)
|
|
|
|
|
this.rangeStatus = { type: 'error', text: `超出范围约${this.formatDistanceVal(over)}` };
|
|
|
|
|
this.canCheckin = false;
|
|
|
|
|
this.alertInfo = { type: 'warning', message: `您需要进入${this.formatDistanceVal(allowed)}范围内才能签到。` };
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 签到
|
|
|
|
|
async signCheck(batch_sign = 0) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await this.$u.api.signCheck({
|
|
|
|
|
course_content_id: this.course_content_id,
|
|
|
|
|
latitude: this.userLocation.lat,
|
|
|
|
|
longitude: this.userLocation.lng,
|
|
|
|
|
batch_sign: batch_sign // 新增参数
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 根据返回结果更新本地状态
|
|
|
|
|
this.hasCheckedIn = true
|
|
|
|
|
this.canCheckin = false
|
|
|
|
|
|
|
|
|
|
// 根据批量签到状态显示不同的成功提示
|
|
|
|
|
if (batch_sign === 1) {
|
|
|
|
|
this.alertInfo = { type: 'success', message: '批量签到成功!已签到所有课程' }
|
|
|
|
|
uni.showToast({ title: '批量签到成功!', icon: 'success' });
|
|
|
|
|
} else {
|
|
|
|
|
this.alertInfo = { type: 'success', message: '签到成功' }
|
|
|
|
|
uni.showToast({ title: '签到成功!', icon: 'success' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 可选:重新拉取签到记录
|
|
|
|
|
try { await this.signGet() } catch (e) {}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('签到失败:', e);
|
|
|
|
|
uni.showToast({ title: '签到失败', icon: 'none' });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async initializeCheckin() {
|
|
|
|
|
this.locationStatus = { type: 'warning', text: '定位中...' };
|
|
|
|
|
this.rangeStatus = { type: 'default', text: '未计算' };
|
|
|
|
|
this.canCheckin = false;
|
|
|
|
|
this.distance = null;
|
|
|
|
|
this.alertInfo = { type: '', message: '' };
|
|
|
|
|
try {
|
|
|
|
|
// 1. 定位
|
|
|
|
|
const loc = await this.getLocation()
|
|
|
|
|
console.log(loc)
|
|
|
|
|
this.userLocation = loc
|
|
|
|
|
this.locationStatus = { type: 'success', text: '定位成功' };
|
|
|
|
|
// 2. 距离与状态
|
|
|
|
|
await this.signDistance()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.locationStatus = { type: 'error', text: '定位失败' };
|
|
|
|
|
this.rangeStatus = { type: 'error', text: '无法计算' };
|
|
|
|
|
this.alertInfo = { type: 'error', message: '获取信息或位置失败,请检查权限和网络。' };
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 坐标转换工具方法
|
|
|
|
|
// WGS84 转 GCJ-02 (火星坐标系)
|
|
|
|
|
wgs84ToGcj02(lng, lat) {
|
|
|
|
|
const a = 6378245.0;
|
|
|
|
|
const ee = 0.00669342162296594323;
|
|
|
|
|
|
|
|
|
|
if (this.outOfChina(lng, lat)) {
|
|
|
|
|
return [lng, lat];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let dLat = this.transformLat(lng - 105.0, lat - 35.0);
|
|
|
|
|
let dLng = this.transformLng(lng - 105.0, lat - 35.0);
|
|
|
|
|
|
|
|
|
|
const radLat = lat / 180.0 * Math.PI;
|
|
|
|
|
let magic = Math.sin(radLat);
|
|
|
|
|
magic = 1 - ee * magic * magic;
|
|
|
|
|
const sqrtMagic = Math.sqrt(magic);
|
|
|
|
|
|
|
|
|
|
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);
|
|
|
|
|
dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI);
|
|
|
|
|
|
|
|
|
|
return [lng + dLng, lat + dLat];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// GCJ-02 转 WGS84
|
|
|
|
|
gcj02ToWgs84(lng, lat) {
|
|
|
|
|
if (this.outOfChina(lng, lat)) {
|
|
|
|
|
return [lng, lat];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let dLat = this.transformLat(lng - 105.0, lat - 35.0);
|
|
|
|
|
let dLng = this.transformLng(lng - 105.0, lat - 35.0);
|
|
|
|
|
|
|
|
|
|
const radLat = lat / 180.0 * Math.PI;
|
|
|
|
|
let magic = Math.sin(radLat);
|
|
|
|
|
magic = 1 - 0.00669342162296594323 * magic * magic;
|
|
|
|
|
const sqrtMagic = Math.sqrt(magic);
|
|
|
|
|
|
|
|
|
|
dLat = (dLat * 180.0) / ((6378245.0 * (1 - 0.00669342162296594323)) / (magic * sqrtMagic) * Math.PI);
|
|
|
|
|
dLng = (dLng * 180.0) / (6378245.0 / sqrtMagic * Math.cos(radLat) * Math.PI);
|
|
|
|
|
|
|
|
|
|
return [lng - dLng, lat - dLat];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 判断是否在中国境内
|
|
|
|
|
outOfChina(lng, lat) {
|
|
|
|
|
return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 转换纬度
|
|
|
|
|
transformLat(lng, lat) {
|
|
|
|
|
let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
|
|
|
|
|
ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
|
|
|
|
|
ret += (20.0 * Math.sin(lat * Math.PI) + 40.0 * Math.sin(lat / 3.0 * Math.PI)) * 2.0 / 3.0;
|
|
|
|
|
ret += (160.0 * Math.sin(lat / 12.0 * Math.PI) + 320 * Math.sin(lat * Math.PI / 30.0)) * 2.0 / 3.0;
|
|
|
|
|
return ret;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 转换经度
|
|
|
|
|
transformLng(lng, lat) {
|
|
|
|
|
let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lat));
|
|
|
|
|
ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
|
|
|
|
|
ret += (20.0 * Math.sin(lng * Math.PI) + 40.0 * Math.sin(lng / 3.0 * Math.PI)) * 2.0 / 3.0;
|
|
|
|
|
ret += (150.0 * Math.sin(lng / 12.0 * Math.PI) + 300.0 * Math.sin(lng / 30.0 * Math.PI)) * 2.0 / 3.0;
|
|
|
|
|
return ret;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 统一坐标转换方法
|
|
|
|
|
convertCoordinates(lng, lat, fromSystem = 'gcj02', toSystem = 'gcj02') {
|
|
|
|
|
if (fromSystem === toSystem) {
|
|
|
|
|
return [lng, lat];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fromSystem === 'wgs84' && toSystem === 'gcj02') {
|
|
|
|
|
return this.wgs84ToGcj02(lng, lat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fromSystem === 'gcj02' && toSystem === 'wgs84') {
|
|
|
|
|
return this.gcj02ToWgs84(lng, lat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果是从WGS84到其他坐标系,先转到GCJ-02
|
|
|
|
|
if (fromSystem === 'wgs84') {
|
|
|
|
|
const gcj02 = this.wgs84ToGcj02(lng, lat);
|
|
|
|
|
return this.convertCoordinates(gcj02[0], gcj02[1], 'gcj02', toSystem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [lng, lat];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 测试坐标转换(开发调试用)
|
|
|
|
|
testCoordinateConversion() {
|
|
|
|
|
// 北京天安门坐标示例
|
|
|
|
|
const wgs84Lng = 116.397128
|
|
|
|
|
const wgs84Lat = 39.916527
|
|
|
|
|
|
|
|
|
|
console.log('=== 坐标转换测试 ===')
|
|
|
|
|
console.log('WGS84坐标:', { lng: wgs84Lng, lat: wgs84Lat })
|
|
|
|
|
|
|
|
|
|
const gcj02 = this.wgs84ToGcj02(wgs84Lng, wgs84Lat)
|
|
|
|
|
console.log('转换为GCJ-02:', { lng: gcj02[0], lat: gcj02[1] })
|
|
|
|
|
|
|
|
|
|
const backToWgs84 = this.gcj02ToWgs84(gcj02[0], gcj02[1])
|
|
|
|
|
console.log('转换回WGS84:', { lng: backToWgs84[0], lat: backToWgs84[1] })
|
|
|
|
|
|
|
|
|
|
const diff = {
|
|
|
|
|
lng: Math.abs(backToWgs84[0] - wgs84Lng),
|
|
|
|
|
lat: Math.abs(backToWgs84[1] - wgs84Lat)
|
|
|
|
|
}
|
|
|
|
|
console.log('转换精度误差:', diff)
|
|
|
|
|
console.log('=== 测试结束 ===')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// calculateDistance 已并入 signDistance
|
|
|
|
|
getDistanceFromLatLonInM(lat1, lon1, lat2, lon2) {
|
|
|
|
|
const R = 6371;
|
|
|
|
|
const dLat = this.deg2rad(lat2 - lat1);
|
|
|
|
|
const dLon = this.deg2rad(lon2 - lon1);
|
|
|
|
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
|
|
|
Math.cos(this.deg2rad(lat1)) * Math.cos(this.deg2rad(lat2)) *
|
|
|
|
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
|
return R * c * 1000;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
deg2rad(deg) {
|
|
|
|
|
return deg * (Math.PI / 180);
|
|
|
|
|
},
|
|
|
|
|
formatDistanceVal(m) {
|
|
|
|
|
if (m == null) return ''
|
|
|
|
|
const n = Number(m)
|
|
|
|
|
if (isNaN(n)) return ''
|
|
|
|
|
if (n >= 1000) return `${Math.round(n / 1000)}km`
|
|
|
|
|
return `${Math.round(n)}m`
|
|
|
|
|
},
|
|
|
|
|
getStatusIcon(type) {
|
|
|
|
|
const icons = {
|
|
|
|
|
success: 'checkmark-circle-fill',
|
|
|
|
|
error: 'close-circle-fill',
|
|
|
|
|
warning: 'error-circle-fill',
|
|
|
|
|
default: 'question-circle-fill'
|
|
|
|
|
};
|
|
|
|
|
return icons[type] || 'question-circle-fill';
|
|
|
|
|
},
|
|
|
|
|
async performCheckin() {
|
|
|
|
|
if (this.hasCheckedIn) {
|
|
|
|
|
uni.showToast({ title: '您今天已经签过到了', icon: 'none' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!this.canCheckin) return
|
|
|
|
|
|
|
|
|
|
// 检查是否有其他可签到课程
|
|
|
|
|
if (this.courseContentList.length > 0) {
|
|
|
|
|
// 弹出确认对话框询问是否要一次性合并签到所有课程
|
|
|
|
|
console.log(this.courseContentList)
|
|
|
|
|
uni.showModal({
|
|
|
|
|
title: '批量签到提示',
|
|
|
|
|
content: `检测到您还有${this.courseContentList.length}门课程可以签到,是否要一次性合并签到所有课程?`,
|
|
|
|
|
confirmText: '批量签到',
|
|
|
|
|
cancelText: '单个签到',
|
|
|
|
|
success: async (res) => {
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
// 用户选择合并签到
|
|
|
|
|
await this.signCheck(1);
|
|
|
|
|
} else {
|
|
|
|
|
// 用户选择仅签到当前课程
|
|
|
|
|
await this.signCheck(0);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.log(err)
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 没有其他课程,直接签到
|
|
|
|
|
await this.signCheck(0);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
loadCheckinHistory() {
|
|
|
|
|
const records = uni.getStorageSync('checkinHistory') || [];
|
|
|
|
|
this.checkinHistory = records;
|
|
|
|
|
if (records.length > 0) {
|
|
|
|
|
const lastRecordTime = new Date(records[0].time);
|
|
|
|
|
if (lastRecordTime.toDateString() === new Date().toDateString()) {
|
|
|
|
|
this.hasCheckedIn = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.checkin-page {
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
}
|
|
|
|
|
.checkin-container {
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
.course-card, .status-card {
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 32rpx;
|
|
|
|
|
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.1);
|
|
|
|
|
padding: 32rpx;
|
|
|
|
|
margin-bottom: 30rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 其他可签到课程中的课程卡片样式 */
|
|
|
|
|
.status-card .course-card {
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
border: 1rpx solid #e9ecef;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-card .course-card:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-card .course-card .course-title {
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-card .course-card .course-info {
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-card .course-card .info-item {
|
|
|
|
|
padding: 10rpx;
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
}
|
|
|
|
|
.course-title {
|
|
|
|
|
font-size: 36rpx;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
}
|
|
|
|
|
.course-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
}
|
|
|
|
|
.info-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
padding: 12rpx;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
}
|
|
|
|
|
.info-icon {
|
|
|
|
|
color: #3498db;
|
|
|
|
|
}
|
|
|
|
|
.card-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
}
|
|
|
|
|
.status-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 16rpx 0;
|
|
|
|
|
border-bottom: 1rpx solid #e9ecef;
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
}
|
|
|
|
|
.status-item:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
.status-label {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
.status-value {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
}
|
|
|
|
|
.status-success { color: #2ecc71; }
|
|
|
|
|
.status-error { color: #e74c3c; }
|
|
|
|
|
.status-warning { color: #f1c40f; }
|
|
|
|
|
.status-default { color: #909399; }
|
|
|
|
|
|
|
|
|
|
.distance-info {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 32rpx;
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
}
|
|
|
|
|
.distance-value {
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #3498db;
|
|
|
|
|
}
|
|
|
|
|
.distance-label {
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
}
|
|
|
|
|
.alert-custom {
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
.alert-success { background: #2ecc71; }
|
|
|
|
|
.alert-error { background: #e74c3c; }
|
|
|
|
|
.alert-warning { background: #f1c40f; }
|
|
|
|
|
|
|
|
|
|
.checkin-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
margin-bottom: 30rpx;
|
|
|
|
|
}
|
|
|
|
|
.checkin-btn /deep/ .u-btn, .recheck-btn /deep/ .u-btn {
|
|
|
|
|
height: 90rpx;
|
|
|
|
|
}
|
|
|
|
|
.btn-icon {
|
|
|
|
|
margin-right: 12rpx;
|
|
|
|
|
}
|
|
|
|
|
.history-empty {
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #999;
|
|
|
|
|
padding: 30rpx 0;
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
}
|
|
|
|
|
.history-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
}
|
|
|
|
|
.history-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
color: #666;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
padding: 12rpx;
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
}
|
|
|
|
|
</style>
|