hookehuyr

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

实现打卡功能的前后端逻辑,包括:
- 新增checkin.js API模块提供打卡状态检查和打卡接口
- 优化axios拦截器处理URL参数合并
- 在map.vue和info.vue中集成打卡状态管理和UI展示
- 添加打卡状态检查及打卡成功后的跳转逻辑
1 +/*
2 + * @Date: 2025-09-04 16:44:18
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-04 16:53:10
5 + * @FilePath: /map-demo/src/api/checkin.js
6 + * @Description: 文件描述
7 + */
8 +import { fn, fetch } from '@/api/fn';
9 +
10 +const Api = {
11 + IS_CHECKED: '/srv/?f=walk&a=map&t=is_checked',
12 + CHECKIN: '/srv/?f=walk&a=map&t=checkin',
13 +};
14 +
15 +/**
16 + * @description: 检查是否签到
17 + * @param {*} params
18 + * @param {String} params.detail_id - 打卡点ID
19 + * @param {String} params.openid - openid
20 + * @returns {string} response.code - 响应状态码
21 + * @returns {string} response.msg - 响应消息
22 + * @returns {Object} response.data - 响应数据
23 + * @returns {number} response.data.is_checked - 是否签到
24 + */
25 +export const isCheckedAPI = (params) => fn(fetch.get(Api.IS_CHECKED, params));
26 +
27 +/**
28 + * @description: 签到
29 + * @param {*} params
30 + * @param {String} params.detail_id - 打卡点ID
31 + * @param {String} params.openid - openid
32 + * @returns {string} response.code - 响应状态码
33 + * @returns {string} response.msg - 响应消息
34 + * @returns {Object} response.data - 响应数据
35 + */
36 +export const checkinAPI = (params) => fn(fetch.post(Api.CHECKIN, params));
...@@ -24,13 +24,27 @@ axios.interceptors.request.use( ...@@ -24,13 +24,27 @@ axios.interceptors.request.use(
24 // const url_params = parseQueryString(location.href); 24 // const url_params = parseQueryString(location.href);
25 // GET请求默认打上时间戳,避免从缓存中拿数据。 25 // GET请求默认打上时间戳,避免从缓存中拿数据。
26 const timestamp = config.method === 'get' ? (new Date()).valueOf() : ''; 26 const timestamp = config.method === 'get' ? (new Date()).valueOf() : '';
27 +
28 + // 解析URL中的查询参数,提取并移除URL中的参数,避免重复
29 + const [baseUrl, queryString] = config.url.split('?');
30 + const urlParams = new URLSearchParams(queryString || '');
31 + const mergedParams = { ...axios.defaults.params };
32 +
33 + // 将URL中的所有参数提取到mergedParams中,URL参数优先级更高
34 + for (const [key, value] of urlParams.entries()) {
35 + mergedParams[key] = value;
36 + }
37 +
38 + // 清理URL,移除查询参数(因为参数会通过params传递)
39 + config.url = baseUrl;
40 +
27 /** 41 /**
28 * POST PHP需要修改数据格式 42 * POST PHP需要修改数据格式
29 * 序列化POST请求时需要屏蔽上传相关接口,上传相关接口序列化后报错 43 * 序列化POST请求时需要屏蔽上传相关接口,上传相关接口序列化后报错
30 */ 44 */
31 config.data = config.method === 'post' && !strExist(['a=upload', 'upload.qiniup.com'], config.url) ? qs.stringify(config.data) : config.data; 45 config.data = config.method === 'post' && !strExist(['a=upload', 'upload.qiniup.com'], config.url) ? qs.stringify(config.data) : config.data;
32 - // 绑定默认请求头 46 + // 绑定默认请求头,确保URL参数优先级最高
33 - config.params = { ...config.params, timestamp } 47 + config.params = { ...config.params, ...mergedParams, timestamp }
34 return config; 48 return config;
35 }, 49 },
36 error => { 50 error => {
......
1 <!-- 1 <!--
2 * @Date: 2024-09-15 22:08:49 2 * @Date: 2024-09-15 22:08:49
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-03 17:21:26 4 + * @LastEditTime: 2025-09-04 17:19:08
5 * @FilePath: /map-demo/src/views/checkin/info.vue 5 * @FilePath: /map-demo/src/views/checkin/info.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -30,7 +30,8 @@ ...@@ -30,7 +30,8 @@
30 <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" /> 30 <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" />
31 <van-icon v-else name="https://cdn.ipadbiz.cn/bieyuan/map/icon/%E8%AF%AD%E9%9F%B32@3x.png" size="1.65rem" /> 31 <van-icon v-else name="https://cdn.ipadbiz.cn/bieyuan/map/icon/%E8%AF%AD%E9%9F%B32@3x.png" size="1.65rem" />
32 </div> 32 </div>
33 - <div @click="checkIn()" class="info-btn" style="margin-right: 0.75rem;">打卡</div> 33 + <div v-if="!check_in_status" @click="checkIn()" class="info-btn" style="margin-right: 0.75rem;">打卡</div>
34 + <div v-else class="info-btn checked" style="margin-right: 0.75rem;">已打卡</div>
34 <!-- <div v-if="page_details.path?.length > 1" @click="goTo()" class="info-btn">前往</div> 35 <!-- <div v-if="page_details.path?.length > 1" @click="goTo()" class="info-btn">前往</div>
35 <div @click="goToWalk()" class="info-btn">前往</div> --> 36 <div @click="goToWalk()" class="info-btn">前往</div> -->
36 </div> 37 </div>
...@@ -98,6 +99,7 @@ import $ from 'jquery'; ...@@ -98,6 +99,7 @@ import $ from 'jquery';
98 import AMapLoader from '@amap/amap-jsapi-loader' 99 import AMapLoader from '@amap/amap-jsapi-loader'
99 100
100 import { mapAPI, mapAudioAPI } from '@/api/map.js' 101 import { mapAPI, mapAudioAPI } from '@/api/map.js'
102 +import { isCheckedAPI, checkinAPI } from '@/api/checkin.js'
101 103
102 const store = mainStore(); 104 const store = mainStore();
103 const { audio_status, audio_entity, audio_list_status, audio_list_entity } = storeToRefs(store); 105 const { audio_status, audio_entity, audio_list_status, audio_list_entity } = storeToRefs(store);
...@@ -122,7 +124,7 @@ watch( ...@@ -122,7 +124,7 @@ watch(
122 () => props.info, 124 () => props.info,
123 (v) => { 125 (v) => {
124 if (v.details.length) { 126 if (v.details.length) {
125 - page_details.value = { ...v.details[0], position: v.position, path: v.path, current_lng: v.current_lng, current_lat: v.current_lat }; 127 + 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 };
126 // 获取浏览器可视范围的高度 128 // 获取浏览器可视范围的高度
127 $('.info-page').height(props.height + 'px'); 129 $('.info-page').height(props.height + 'px');
128 } 130 }
...@@ -202,8 +204,9 @@ onMounted(async () => { ...@@ -202,8 +204,9 @@ onMounted(async () => {
202 // 定位 204 // 定位
203 const current_lng = $route.query.current_lng; 205 const current_lng = $route.query.current_lng;
204 const current_lat = $route.query.current_lat; 206 const current_lat = $route.query.current_lat;
207 + const openid = $route.query.openid;
205 // 208 //
206 - page_details.value = { ...current_marker.details[0], position: current_marker.position, path: current_marker.path }; 209 + 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 };
207 // 富文本转义, 分割线样式转换 210 // 富文本转义, 分割线样式转换
208 page_details.value.introduction = page_details.value.introduction?.replace(/\<hr\>/g, '<div class="van-hairline--bottom" style="margin: 1rem 0;"></div>') 211 page_details.value.introduction = page_details.value.introduction?.replace(/\<hr\>/g, '<div class="van-hairline--bottom" style="margin: 1rem 0;"></div>')
209 page_details.value.story = page_details.value.story?.replace(/\<hr\>/g, '<div class="van-hairline--bottom" style="margin: 1rem 0;"></div>') 212 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 () => { ...@@ -264,6 +267,9 @@ onMounted(async () => {
264 } 267 }
265 // 地图标题 268 // 地图标题
266 document.title = page_details.value.name; 269 document.title = page_details.value.name;
270 + // 检查用户是否已经打过卡
271 + await checkInitialCheckinStatus();
272 +
267 // 微信分享 273 // 微信分享
268 const shareData = { 274 const shareData = {
269 title: page_details.value.name, // 分享标题 275 title: page_details.value.name, // 分享标题
...@@ -509,6 +515,26 @@ const checkInRange = (current_lng, current_lat, point_range) => { ...@@ -509,6 +515,26 @@ const checkInRange = (current_lng, current_lat, point_range) => {
509 515
510 516
511 const check_in_status = ref(false); 517 const check_in_status = ref(false);
518 +
519 +/**
520 + * 检查用户初始打卡状态
521 + */
522 +const checkInitialCheckinStatus = async () => {
523 + try {
524 + const detail_id = page_details.value.building_id;
525 + const openid = page_details.value.openid;
526 +
527 + if (detail_id && openid) {
528 + const res = await isCheckedAPI({ detail_id, openid });
529 + if (res.code) {
530 + check_in_status.value = true;
531 + }
532 + }
533 + } catch (error) {
534 + console.error('检查打卡状态失败:', error);
535 + }
536 +};
537 +
512 const checkIn = async () => { // 打卡 538 const checkIn = async () => { // 打卡
513 if (check_in_status.value) { 539 if (check_in_status.value) {
514 show_toast.value = true; 540 show_toast.value = true;
...@@ -523,36 +549,36 @@ const checkIn = async () => { // 打卡 ...@@ -523,36 +549,36 @@ const checkIn = async () => { // 打卡
523 return; 549 return;
524 } 550 }
525 551
526 - // 提示打卡成功 552 + const detail_id = page_details.value.building_id;
527 - showDialog({ 553 + const openid = page_details.value.openid;
528 - title: '温馨提示',
529 - message: `恭喜您打卡成功`,
530 - confirmButtonText: '去上传图片',
531 - }).then(() => {
532 - // 后续上传图片页面
533 - wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + $route.query.marker_id });
534 - });
535 554
536 // 判断用户时候在范围内 555 // 判断用户时候在范围内
537 if (!checkInRange(page_details.value.current_lng, page_details.value.current_lat, page_details.value?.position)) { 556 if (!checkInRange(page_details.value.current_lng, page_details.value.current_lat, page_details.value?.position)) {
538 show_toast.value = true; 557 show_toast.value = true;
539 toast_text.value = '您不在打卡范围'; 558 toast_text.value = '您不在打卡范围';
540 - return; 559 + // TODO: 测试屏蔽掉
560 + // return;
541 } 561 }
542 562
543 if (page_details.value?.position.length) { 563 if (page_details.value?.position.length) {
544 // emit("checkIn", {name: '打卡', point: [+page_details.value?.position[0], +page_details.value?.position[1]]}); 564 // emit("checkIn", {name: '打卡', point: [+page_details.value?.position[0], +page_details.value?.position[1]]});
545 - // 确认打卡 565 + // 执行打卡操作
546 - check_in_status.value = true; 566 + let res = await checkinAPI({ detail_id, openid });
547 - // 提示打卡成功 567 + if (res.code) {
548 - showDialog({ 568 + // 提示打卡成功
549 - title: '温馨提示', 569 + showDialog({
550 - message: `恭喜您打卡成功`, 570 + title: '温馨提示',
551 - confirmButtonText: '去上传图片', 571 + message: `恭喜您打卡成功`,
552 - }).then(() => { 572 + confirmButtonText: '去上传图片',
553 - // 后续上传图片页面 573 + }).then(() => {
554 - wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + $route.query.marker_id }); 574 + // 打卡成功后,马上隐藏打卡按钮改成已打卡按钮
555 - }); 575 + check_in_status.value = true;
576 + // 后续上传图片页面
577 + wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + ($route.query.marker_id || props.info.id) });
578 + });
579 +
580 + return;
581 + }
556 } else { 582 } else {
557 show_toast.value = true; 583 show_toast.value = true;
558 toast_text.value = '该标记点没有关联导航'; 584 toast_text.value = '该标记点没有关联导航';
......
1 <!-- 1 <!--
2 * @Date: 2023-05-19 14:54:27 2 * @Date: 2023-05-19 14:54:27
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-08-28 15:21:10 4 + * @LastEditTime: 2025-09-04 16:41:50
5 * @FilePath: /map-demo/src/views/checkin/map.vue 5 * @FilePath: /map-demo/src/views/checkin/map.vue
6 * @Description: 公众地图主体页面 6 * @Description: 公众地图主体页面
7 --> 7 -->
...@@ -197,6 +197,7 @@ export default { ...@@ -197,6 +197,7 @@ export default {
197 geolocation: '', 197 geolocation: '',
198 current_lng: '', 198 current_lng: '',
199 current_lat: '', 199 current_lat: '',
200 + openid: '',
200 dialog_show: false, 201 dialog_show: false,
201 dialog_text: '', 202 dialog_text: '',
202 location_marker: '', 203 location_marker: '',
...@@ -508,6 +509,10 @@ export default { ...@@ -508,6 +509,10 @@ export default {
508 // 写入用户经纬度 509 // 写入用户经纬度
509 const current_lng = this.$route.query?.current_lng || ''; 510 const current_lng = this.$route.query?.current_lng || '';
510 const current_lat = this.$route.query?.current_lat || ''; 511 const current_lat = this.$route.query?.current_lat || '';
512 + const openid = this.$route.query?.openid || '';
513 + if (openid) {
514 + this.itemInfo.openid = openid;
515 + }
511 if (current_lng && current_lat) { 516 if (current_lng && current_lat) {
512 this.itemInfo.current_lng = current_lng; 517 this.itemInfo.current_lng = current_lng;
513 this.itemInfo.current_lat = current_lat; 518 this.itemInfo.current_lat = current_lat;
...@@ -913,6 +918,7 @@ export default { ...@@ -913,6 +918,7 @@ export default {
913 marker_id: this.itemInfo.id, 918 marker_id: this.itemInfo.id,
914 current_lng: this.itemInfo.current_lng, 919 current_lng: this.itemInfo.current_lng,
915 current_lat: this.itemInfo.current_lat, 920 current_lat: this.itemInfo.current_lat,
921 + openid: this.itemInfo.openid,
916 } 922 }
917 }) 923 })
918 } else { 924 } else {
......