feat(打卡): 新增打卡功能及相关API接口
实现打卡功能的前后端逻辑,包括: - 新增checkin.js API模块提供打卡状态检查和打卡接口 - 优化axios拦截器处理URL参数合并 - 在map.vue和info.vue中集成打卡状态管理和UI展示 - 添加打卡状态检查及打卡成功后的跳转逻辑
Showing
4 changed files
with
102 additions
and
20 deletions
src/api/checkin.js
0 → 100644
| 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 }); |
| 567 | + if (res.code) { | ||
| 547 | // 提示打卡成功 | 568 | // 提示打卡成功 |
| 548 | showDialog({ | 569 | showDialog({ |
| 549 | title: '温馨提示', | 570 | title: '温馨提示', |
| 550 | message: `恭喜您打卡成功`, | 571 | message: `恭喜您打卡成功`, |
| 551 | confirmButtonText: '去上传图片', | 572 | confirmButtonText: '去上传图片', |
| 552 | }).then(() => { | 573 | }).then(() => { |
| 574 | + // 打卡成功后,马上隐藏打卡按钮改成已打卡按钮 | ||
| 575 | + check_in_status.value = true; | ||
| 553 | // 后续上传图片页面 | 576 | // 后续上传图片页面 |
| 554 | - wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + $route.query.marker_id }); | 577 | + wx.miniProgram.navigateTo({ url: '/pages/UploadMedia/index?from=checkin&id=' + $route.query.id + '&marker_id=' + ($route.query.marker_id || props.info.id) }); |
| 555 | }); | 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 { | ... | ... |
-
Please register or login to post a comment