You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
355 lines
9.9 KiB
355 lines
9.9 KiB
|
5 months ago
|
<template>
|
||
|
|
<view class="checkin-page">
|
||
|
|
<view class="checkin-container">
|
||
|
|
<!-- 课程信息卡片 -->
|
||
|
|
<view class="course-card">
|
||
|
|
<view class="course-title">{{ course.title }}</view>
|
||
|
|
<view class="course-info">
|
||
|
|
<view class="info-item">
|
||
|
|
<u-icon name="calendar-fill" class="info-icon"></u-icon>
|
||
|
|
<text>{{ course.time }}</text>
|
||
|
|
</view>
|
||
|
|
<view class="info-item">
|
||
|
|
<u-icon name="map-fill" class="info-icon"></u-icon>
|
||
|
|
<text>{{ course.locationName }}</text>
|
||
|
|
</view>
|
||
|
|
<view class="info-item">
|
||
|
|
<u-icon name="account-fill" class="info-icon"></u-icon>
|
||
|
|
<text>{{ course.teacher }}</text>
|
||
|
|
</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">{{ distance }} m</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="initializeCheckin"
|
||
|
|
>
|
||
|
|
<u-icon name="reload" class="btn-icon"></u-icon>
|
||
|
|
重新定位
|
||
|
|
</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.time }}</text>
|
||
|
|
<text>距离: {{ record.distance }}m</text>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
export default {
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
course: {
|
||
|
|
title: '2025产业加速营 | 智能制造专题',
|
||
|
|
time: '2025-06-04 09:00-17:00',
|
||
|
|
teacher: '王教授',
|
||
|
|
locationName: '商学院A101',
|
||
|
|
location: { lat: 31.2741, lng: 120.7853 },
|
||
|
|
allowedDistance: 100 // 米
|
||
|
|
},
|
||
|
|
userLocation: null,
|
||
|
|
distance: null,
|
||
|
|
canCheckin: false,
|
||
|
|
hasCheckedIn: false,
|
||
|
|
locationStatus: { type: 'default', text: '未定位' },
|
||
|
|
rangeStatus: { type: 'default', text: '未计算' },
|
||
|
|
alertInfo: { type: '', message: '' },
|
||
|
|
checkinHistory: []
|
||
|
|
};
|
||
|
|
},
|
||
|
|
onLoad() {
|
||
|
|
this.loadCheckinHistory();
|
||
|
|
this.initializeCheckin();
|
||
|
|
},
|
||
|
|
methods: {
|
||
|
|
initializeCheckin() {
|
||
|
|
this.locationStatus = { type: 'warning', text: '定位中...' };
|
||
|
|
this.rangeStatus = { type: 'default', text: '未计算' };
|
||
|
|
this.canCheckin = false;
|
||
|
|
this.distance = null;
|
||
|
|
this.alertInfo = { type: '', message: '' };
|
||
|
|
|
||
|
|
uni.getLocation({
|
||
|
|
type: 'gcj02',
|
||
|
|
success: (res) => {
|
||
|
|
this.userLocation = { lat: res.latitude, lng: res.longitude };
|
||
|
|
this.locationStatus = { type: 'success', text: '定位成功' };
|
||
|
|
this.calculateDistance();
|
||
|
|
},
|
||
|
|
fail: () => {
|
||
|
|
this.locationStatus = { type: 'error', text: '定位失败' };
|
||
|
|
this.rangeStatus = { type: 'error', text: '无法计算' };
|
||
|
|
this.alertInfo = { type: 'error', message: '获取位置失败,请检查定位权限和网络。' };
|
||
|
|
}
|
||
|
|
});
|
||
|
|
},
|
||
|
|
calculateDistance() {
|
||
|
|
if (!this.userLocation) return;
|
||
|
|
const dist = this.getDistanceFromLatLonInM(
|
||
|
|
this.userLocation.lat, this.userLocation.lng,
|
||
|
|
this.course.location.lat, this.course.location.lng
|
||
|
|
);
|
||
|
|
this.distance = Math.round(dist);
|
||
|
|
|
||
|
|
if (this.distance <= this.course.allowedDistance) {
|
||
|
|
this.rangeStatus = { type: 'success', text: '在打卡范围内' };
|
||
|
|
this.canCheckin = !this.hasCheckedIn;
|
||
|
|
this.alertInfo = { type: 'success', message: `您已进入打卡范围,距离${this.distance}米。` };
|
||
|
|
} else {
|
||
|
|
this.rangeStatus = { type: 'error', text: `超出范围约${this.distance - this.course.allowedDistance}米` };
|
||
|
|
this.canCheckin = false;
|
||
|
|
this.alertInfo = { type: 'warning', message: `您需要进入${this.course.allowedDistance}米范围内才能签到。` };
|
||
|
|
}
|
||
|
|
},
|
||
|
|
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);
|
||
|
|
},
|
||
|
|
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';
|
||
|
|
},
|
||
|
|
performCheckin() {
|
||
|
|
if (this.hasCheckedIn) {
|
||
|
|
uni.showToast({ title: '您今天已经签过到了', icon: 'none' });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const now = new Date();
|
||
|
|
const checkinData = {
|
||
|
|
type: '位置签到',
|
||
|
|
time: now.toLocaleString('zh-CN'),
|
||
|
|
distance: this.distance
|
||
|
|
};
|
||
|
|
|
||
|
|
this.checkinHistory.unshift(checkinData);
|
||
|
|
uni.setStorageSync('checkinHistory', this.checkinHistory);
|
||
|
|
this.hasCheckedIn = true;
|
||
|
|
this.canCheckin = false;
|
||
|
|
this.alertInfo = { type: 'success', message: `签到成功!时间:${checkinData.time}` };
|
||
|
|
uni.showToast({ title: '签到成功!', icon: 'success' });
|
||
|
|
},
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
.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>
|