hookehuyr

feat(打卡): 新增打卡功能及相关API接口

实现打卡功能的前后端逻辑,包括:
- 新增checkin.js API模块提供打卡状态检查和打卡接口
- 优化axios拦截器处理URL参数合并
- 在map.vue和info.vue中集成打卡状态管理和UI展示
- 添加打卡状态检查及打卡成功后的跳转逻辑
/*
* @Date: 2025-09-04 16:44:18
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-04 16:53:10
* @FilePath: /map-demo/src/api/checkin.js
* @Description: 文件描述
*/
import { fn, fetch } from '@/api/fn';
const Api = {
IS_CHECKED: '/srv/?f=walk&a=map&t=is_checked',
CHECKIN: '/srv/?f=walk&a=map&t=checkin',
};
/**
* @description: 检查是否签到
* @param {*} params
* @param {String} params.detail_id - 打卡点ID
* @param {String} params.openid - openid
* @returns {string} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
* @returns {Object} response.data - 响应数据
* @returns {number} response.data.is_checked - 是否签到
*/
export const isCheckedAPI = (params) => fn(fetch.get(Api.IS_CHECKED, params));
/**
* @description: 签到
* @param {*} params
* @param {String} params.detail_id - 打卡点ID
* @param {String} params.openid - openid
* @returns {string} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
* @returns {Object} response.data - 响应数据
*/
export const checkinAPI = (params) => fn(fetch.post(Api.CHECKIN, params));
......@@ -24,13 +24,27 @@ axios.interceptors.request.use(
// const url_params = parseQueryString(location.href);
// GET请求默认打上时间戳,避免从缓存中拿数据。
const timestamp = config.method === 'get' ? (new Date()).valueOf() : '';
// 解析URL中的查询参数,提取并移除URL中的参数,避免重复
const [baseUrl, queryString] = config.url.split('?');
const urlParams = new URLSearchParams(queryString || '');
const mergedParams = { ...axios.defaults.params };
// 将URL中的所有参数提取到mergedParams中,URL参数优先级更高
for (const [key, value] of urlParams.entries()) {
mergedParams[key] = value;
}
// 清理URL,移除查询参数(因为参数会通过params传递)
config.url = baseUrl;
/**
* POST PHP需要修改数据格式
* 序列化POST请求时需要屏蔽上传相关接口,上传相关接口序列化后报错
*/
config.data = config.method === 'post' && !strExist(['a=upload', 'upload.qiniup.com'], config.url) ? qs.stringify(config.data) : config.data;
// 绑定默认请求头
config.params = { ...config.params, timestamp }
// 绑定默认请求头,确保URL参数优先级最高
config.params = { ...config.params, ...mergedParams, timestamp }
return config;
},
error => {
......
<!--
* @Date: 2024-09-15 22:08:49
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-03 17:21:26
* @LastEditTime: 2025-09-04 17:19:08
* @FilePath: /map-demo/src/views/checkin/info.vue
* @Description: 文件描述
-->
......@@ -30,7 +30,8 @@
<van-icon v-if="!audio_list_height" name="https://cdn.ipadbiz.cn/bieyuan/map/icon/%E8%AF%AD%E9%9F%B31@3x.png" size="1.65rem" />
<van-icon v-else name="https://cdn.ipadbiz.cn/bieyuan/map/icon/%E8%AF%AD%E9%9F%B32@3x.png" size="1.65rem" />
</div>
<div @click="checkIn()" class="info-btn" style="margin-right: 0.75rem;">打卡</div>
<div v-if="!check_in_status" @click="checkIn()" class="info-btn" style="margin-right: 0.75rem;">打卡</div>
<div v-else class="info-btn checked" style="margin-right: 0.75rem;">已打卡</div>
<!-- <div v-if="page_details.path?.length > 1" @click="goTo()" class="info-btn">前往</div>
<div @click="goToWalk()" class="info-btn">前往</div> -->
</div>
......@@ -98,6 +99,7 @@ import $ from 'jquery';
import AMapLoader from '@amap/amap-jsapi-loader'
import { mapAPI, mapAudioAPI } from '@/api/map.js'
import { isCheckedAPI, checkinAPI } from '@/api/checkin.js'
const store = mainStore();
const { audio_status, audio_entity, audio_list_status, audio_list_entity } = storeToRefs(store);
......@@ -122,7 +124,7 @@ watch(
() => props.info,
(v) => {
if (v.details.length) {
page_details.value = { ...v.details[0], position: v.position, path: v.path, current_lng: v.current_lng, current_lat: v.current_lat };
page_details.value = { ...v.details[0], position: v.position, path: v.path, current_lng: v.current_lng, current_lat: v.current_lat, openid: v.openid };
// 获取浏览器可视范围的高度
$('.info-page').height(props.height + 'px');
}
......@@ -202,8 +204,9 @@ onMounted(async () => {
// 定位
const current_lng = $route.query.current_lng;
const current_lat = $route.query.current_lat;
const openid = $route.query.openid;
//
page_details.value = { ...current_marker.details[0], position: current_marker.position, path: current_marker.path };
page_details.value = { ...current_marker.details[0], position: current_marker.position, path: current_marker.path, current_lng: current_lng, current_lat: current_lat, openid: openid };
// 富文本转义, 分割线样式转换
page_details.value.introduction = page_details.value.introduction?.replace(/\<hr\>/g, '<div class="van-hairline--bottom" style="margin: 1rem 0;"></div>')
page_details.value.story = page_details.value.story?.replace(/\<hr\>/g, '<div class="van-hairline--bottom" style="margin: 1rem 0;"></div>')
......@@ -264,6 +267,9 @@ onMounted(async () => {
}
// 地图标题
document.title = page_details.value.name;
// 检查用户是否已经打过卡
await checkInitialCheckinStatus();
// 微信分享
const shareData = {
title: page_details.value.name, // 分享标题
......@@ -509,6 +515,26 @@ const checkInRange = (current_lng, current_lat, point_range) => {
const check_in_status = ref(false);
/**
* 检查用户初始打卡状态
*/
const checkInitialCheckinStatus = async () => {
try {
const detail_id = page_details.value.building_id;
const openid = page_details.value.openid;
if (detail_id && openid) {
const res = await isCheckedAPI({ detail_id, openid });
if (res.code) {
check_in_status.value = true;
}
}
} catch (error) {
console.error('检查打卡状态失败:', error);
}
};
const checkIn = async () => { // 打卡
if (check_in_status.value) {
show_toast.value = true;
......@@ -523,36 +549,36 @@ const checkIn = async () => { // 打卡
return;
}
// 提示打卡成功
showDialog({
title: '温馨提示',
message: `恭喜您打卡成功`,
confirmButtonText: '去上传图片',
}).then(() => {
// 后续上传图片页面
wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + $route.query.marker_id });
});
const detail_id = page_details.value.building_id;
const openid = page_details.value.openid;
// 判断用户时候在范围内
if (!checkInRange(page_details.value.current_lng, page_details.value.current_lat, page_details.value?.position)) {
show_toast.value = true;
toast_text.value = '您不在打卡范围';
return;
// TODO: 测试屏蔽掉
// return;
}
if (page_details.value?.position.length) {
// emit("checkIn", {name: '打卡', point: [+page_details.value?.position[0], +page_details.value?.position[1]]});
// 确认打卡
check_in_status.value = true;
// 提示打卡成功
showDialog({
title: '温馨提示',
message: `恭喜您打卡成功`,
confirmButtonText: '去上传图片',
}).then(() => {
// 后续上传图片页面
wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + $route.query.marker_id });
});
// 执行打卡操作
let res = await checkinAPI({ detail_id, openid });
if (res.code) {
// 提示打卡成功
showDialog({
title: '温馨提示',
message: `恭喜您打卡成功`,
confirmButtonText: '去上传图片',
}).then(() => {
// 打卡成功后,马上隐藏打卡按钮改成已打卡按钮
check_in_status.value = true;
// 后续上传图片页面
wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + ($route.query.marker_id || props.info.id) });
});
return;
}
} else {
show_toast.value = true;
toast_text.value = '该标记点没有关联导航';
......
<!--
* @Date: 2023-05-19 14:54:27
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-28 15:21:10
* @LastEditTime: 2025-09-04 16:41:50
* @FilePath: /map-demo/src/views/checkin/map.vue
* @Description: 公众地图主体页面
-->
......@@ -197,6 +197,7 @@ export default {
geolocation: '',
current_lng: '',
current_lat: '',
openid: '',
dialog_show: false,
dialog_text: '',
location_marker: '',
......@@ -508,6 +509,10 @@ export default {
// 写入用户经纬度
const current_lng = this.$route.query?.current_lng || '';
const current_lat = this.$route.query?.current_lat || '';
const openid = this.$route.query?.openid || '';
if (openid) {
this.itemInfo.openid = openid;
}
if (current_lng && current_lat) {
this.itemInfo.current_lng = current_lng;
this.itemInfo.current_lat = current_lat;
......@@ -913,6 +918,7 @@ export default {
marker_id: this.itemInfo.id,
current_lng: this.itemInfo.current_lng,
current_lat: this.itemInfo.current_lat,
openid: this.itemInfo.openid,
}
})
} else {
......