Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Hooke
/
lls_program
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Builds
Commits
Issue Boards
Authored by
hookehuyr
2026-05-20 19:01:43 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
a2678b7e50e303b3f03ffec52e98700665ed7706
a2678b7e
1 parent
c15c8944
feat: refine scan checkin detail and list flow
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
234 additions
and
40 deletions
src/pages/ScanCheckinDetail/index.vue
src/pages/ScanCheckinList/index.less
src/pages/ScanCheckinList/index.vue
src/utils/scanCheckin.js
src/pages/ScanCheckinDetail/index.vue
View file @
a2678b7
...
...
@@ -36,7 +36,7 @@
</view>
</view>
<view class="scan-checkin-detail-button-wrap">
<view
v-if="detail.isChecked !== true"
class="scan-checkin-detail-button-wrap">
<nut-button
type="primary"
class="scan-checkin-detail-button"
...
...
@@ -44,7 +44,7 @@
:loading="scanSubmitting"
@click="handleScanCheckin"
>
扫码打卡
{{ actionButtonText }}
</nut-button>
</view>
</view>
...
...
@@ -58,8 +58,13 @@ import RichTextRenderer from '@/components/RichTextRenderer.vue'
import { getCurrentPageFullPath } from '@/utils/authRedirect'
import { getMyFamiliesAPI } from '@/api/family'
import { getScanStageDetailAPI, submitScanCheckinAPI } from '@/api/map'
import { getUserProfileAPI } from '@/api/user'
import { verifyCheckinRangeWithCurrentLocation } from '@/utils/checkinLocation'
import { parseScanCheckinParams, parseScanCheckinSceneParams } from '@/utils/scanCheckin'
import {
parseScanCheckinParams,
parseScanCheckinSceneParams,
isSameScanCheckinTarget,
} from '@/utils/scanCheckin'
const defaultCover =
'https://cdn.ipadbiz.cn/lls_prog/images/check_detail_img.png?imageMogr2/strip/quality/60'
...
...
@@ -70,6 +75,9 @@ const detail = reactive({
regSource: '',
regStageId: '',
returnUrl: '',
openid: '',
entryMode: 'scan_before_submit',
entrySceneRaw: '',
banners: [defaultCover],
title: '',
guideText: '',
...
...
@@ -85,6 +93,10 @@ const detail = reactive({
})
const scanSubmitting = computed(() => detail.scanSubmitting === true)
const actionButtonText = computed(() =>
detail.entryMode === 'direct_submit' ? '点击打卡' : '扫码打卡'
)
const isApiSuccess = code => Number(code) === 1
const normalizedRichTextContent = computed(() => {
const content = detail.discountContentRaw
...
...
@@ -140,12 +152,54 @@ const promptNavigateToWelcome = async () => {
}
}
// 太阳码直达详情页时,当前页已经和目标打卡点对齐,可直接进入提交模式。
const resolveEntryMode = (sceneParams, options = {}) => {
const hasSceneTarget = Boolean(sceneParams.detailId && sceneParams.activityId)
const explicitMode = String(options.checkin_mode || '').trim()
if (explicitMode === 'direct_submit' || hasSceneTarget) {
return 'direct_submit'
}
return 'scan_before_submit'
}
// 打卡接口现在要求显式传 openid,这里统一从用户资料接口补齐并缓存到页面状态。
const ensureOpenid = async () => {
if (detail.openid) {
return detail.openid
}
const profileResult = await getUserProfileAPI()
if (!isApiSuccess(profileResult?.code)) {
Taro.showToast({
title: profileResult?.msg || '获取用户信息失败',
icon: 'none',
})
return ''
}
const openid = String(profileResult?.data?.user?.openid || '').trim()
if (!openid) {
Taro.showToast({
title: '未获取到用户openid',
icon: 'none',
})
return ''
}
detail.openid = openid
return openid
}
const handleScanCheckin = async () => {
detail.scanSubmitting = true
try {
const familyResult = await getMyFamiliesAPI()
const hasFamily =
familyResult?.code
&& familyResult?.data?.families?.length > 0
const hasFamily =
isApiSuccess(familyResult?.code)
&& familyResult?.data?.families?.length > 0
if (!hasFamily) {
detail.scanSubmitting = false
...
...
@@ -177,12 +231,21 @@ const handleScanCheckin = async () => {
return
}
// 太阳码直达详情页时,不需要再扫一次码,直接按当前详情提交即可。
if (detail.entryMode === 'direct_submit') {
await submitCheckin({
activityId: detail.activityId,
detailId: detail.id,
})
return
}
const scanResult = await Taro.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
scanType: ['qrCode', 'barCode'
, 'WX_CODE'
],
})
const scannedCode = scanResult.result || ''
const scannedCode = scanResult.
path || scanResult.
result || ''
if (!scannedCode) {
Taro.showToast({
...
...
@@ -205,38 +268,31 @@ const handleScanCheckin = async () => {
return
}
const submitResult = await submitScanCheckinAPI({
activity_id: submitActivityId,
detail_id: submitDetailId,
})
if (submitResult.code === 1) {
detail.isChecked = true
detail.lastScanCode = scannedCode
// 列表进入详情页时,扫码结果只负责确认目标打卡点,避免在错误详情里静默打错卡。
const matchedCurrentDetail = isSameScanCheckinTarget(
{
activityId: detail.activityId,
detailId: detail.id,
},
{
activityId: submitActivityId,
detailId: submitDetailId,
}
)
const modalResult = await Taro.showModal(
{
title: '打卡成功',
content: '您已完成当前扫码打卡,可前往列表查看状态。'
,
showCancel: false
,
confirmText: '查看列表'
,
if (matchedCurrentDetail)
{
await submitCheckin({
activityId: submitActivityId
,
detailId: submitDetailId
,
scannedCode
,
})
if (modalResult.confirm) {
const params = new URLSearchParams({
activityId: detail.activityId,
})
// 用户确认“查看列表”后回到列表页,方便继续处理同一活动下的其他关卡。
Taro.redirectTo({
url: `/pages/ScanCheckinList/index?${params.toString()}`,
})
}
return
}
Taro.showToast({
title: submitResult.msg || '提交失败',
icon: 'none',
await promptNavigateToMatchedDetail({
activityId: submitActivityId,
detailId: submitDetailId,
rawScene: parsedScanParams.rawScene,
})
} catch (error) {
if (error?.errMsg && error.errMsg.includes('cancel')) {
...
...
@@ -253,6 +309,91 @@ const handleScanCheckin = async () => {
}
}
// 提交动作统一收口到这里,确保 openid、成功提示和列表回跳逻辑保持一致。
const submitCheckin = async ({ activityId, detailId, scannedCode = '' }) => {
if (!activityId || !detailId) {
Taro.showToast({
title: '缺少打卡参数',
icon: 'none',
})
return
}
const openid = await ensureOpenid()
if (!openid) {
return
}
const submitResult = await submitScanCheckinAPI({
activity_id: activityId,
detail_id: detailId,
openid,
})
if (isApiSuccess(submitResult?.code)) {
detail.isChecked = true
detail.lastScanCode = scannedCode || detail.lastScanCode
const modalResult = await Taro.showModal({
title: '打卡成功',
content: '您已完成当前扫码打卡,可前往列表查看状态。',
showCancel: false,
confirmText: '查看列表',
})
if (modalResult.confirm) {
const params = new URLSearchParams({
activityId: activityId || detail.activityId,
})
Taro.redirectTo({
url: `/pages/ScanCheckinList/index?${params.toString()}`,
})
}
return
}
Taro.showToast({
title: submitResult.msg || '提交失败',
icon: 'none',
})
}
// 扫到别的点位时先征求用户确认,再把页面切到目标详情并改成直达提交模式。
const promptNavigateToMatchedDetail = async ({ activityId, detailId, rawScene }) => {
const modalResult = await showMismatchConfirmModal()
if (!modalResult.confirm) {
return
}
const nextParams = new URLSearchParams({
activityId,
detailId,
checkin_mode: 'direct_submit',
})
const nextScene = String(rawScene || '').trim()
if (nextScene) {
nextParams.set('scene', nextScene)
}
Taro.redirectTo({
url: `/pages/ScanCheckinDetail/index?${nextParams.toString()}`,
})
}
const showMismatchConfirmModal = async () => {
return Taro.showModal({
title: '打卡点不匹配',
content: '当前扫码点与本页不匹配,是否前往对应打卡点详情?',
confirmText: '前往详情',
cancelText: '取消',
})
}
const applyStageDetail = stageDetail => {
detail.scanSubmitting = false
...
...
@@ -282,6 +423,8 @@ useLoad(options => {
const detailId = sceneParams.detailId || options.detailId || options.id || ''
detail.regSource = options.reg_source || ''
detail.regStageId = options.reg_stage_id || ''
detail.entrySceneRaw = sceneParams.rawScene || ''
detail.entryMode = resolveEntryMode(sceneParams, options)
// 当前页路径会透传给补资料页,提交成功后用于回跳续扫。
detail.returnUrl = `/${getCurrentPageFullPath()}`
detail.id = detailId
...
...
@@ -294,7 +437,7 @@ const loadStageDetail = async () => {
id: detail.id,
})
if (
result?.code !== 1
|| !result?.data) {
if (
!isApiSuccess(result?.code)
|| !result?.data) {
Taro.showToast({
title: result?.msg || '获取关卡详情失败',
icon: 'none',
...
...
@@ -303,6 +446,11 @@ const loadStageDetail = async () => {
}
applyStageDetail(result.data)
if (!detail.openid) {
// 提前预取 openid,减少用户点击打卡时再补资料接口带来的等待感。
await ensureOpenid()
}
}
</script>
...
...
src/pages/ScanCheckinList/index.less
View file @
a2678b7
.scan-checkin-list-page {
min-height: 100vh;
padding: 32rpx 24rpx 2
2
0rpx;
padding: 32rpx 24rpx 2
6
0rpx;
background: linear-gradient(180deg, #f6f8fb 0%, #eef2f5 100%);
box-sizing: border-box;
position: relative;
...
...
@@ -87,6 +87,17 @@
flex-shrink: 0;
}
.scan-checkin-list-checked-tag {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: rgba(54, 181, 187, 0.12);
color: #2c9ca2;
font-size: 24rpx;
line-height: 1;
font-weight: 500;
flex-shrink: 0;
}
.scan-checkin-list-load-more,
.scan-checkin-list-no-more {
padding: 24rpx 0 8rpx;
...
...
@@ -105,7 +116,7 @@
.scan-checkin-list-floating-button {
position: fixed;
right: 24rpx;
bottom: 1
2
0rpx;
bottom: 1
6
0rpx;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
...
...
src/pages/ScanCheckinList/index.vue
View file @
a2678b7
...
...
@@ -25,6 +25,8 @@
<text class="scan-checkin-list-name">{{ point.title }}</text>
</view>
<text v-if="point.isChecked === true" class="scan-checkin-list-checked-tag">已打卡</text>
<view class="scan-checkin-list-action" @click="goToDetail(point)">
<Scan2 size="20" />
</view>
...
...
@@ -55,6 +57,8 @@
/>
<text class="scan-checkin-list-floating-text">展位图</text>
</view>
<BottomNav />
</view>
</template>
...
...
@@ -63,6 +67,7 @@ import { ref } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import { IconFont, Scan2 } from '@nutui/icons-vue-taro'
import './index.less'
import BottomNav from '@/components/BottomNav.vue'
import { getScanStageListAPI } from '@/api/map'
const pointList = ref([])
...
...
@@ -73,6 +78,7 @@ const hasMore = ref(true)
const currentPage = ref(0)
const pageSize = ref(10)
// 列表页只保留展示所需字段,避免模板层直接感知后端原始命名。
const mapStageList = stageList =>
stageList.map(stage => ({
id: stage.id,
...
...
src/utils/scanCheckin.js
View file @
a2678b7
...
...
@@ -35,8 +35,8 @@ export const parseScanCheckinSceneParams = (rawScene = '') => {
/**
* @description 从扫码结果中提取打卡接口所需参数
* @param {string} rawScanResult - 微信扫码返回的原始结果,可能是完整URL,也可能是纯查询串
* @returns {{activityId:string, detailId:string, rawParams:Object}}
* @param {string} rawScanResult - 微信扫码返回的原始结果,可能是
path、
完整URL,也可能是纯查询串
* @returns {{activityId:string, detailId:string, rawParams:Object
, rawScene:string
}}
*/
export
const
parseScanCheckinParams
=
(
rawScanResult
=
''
)
=>
{
const
normalized
=
String
(
rawScanResult
||
''
).
trim
()
...
...
@@ -46,17 +46,44 @@ export const parseScanCheckinParams = (rawScanResult = '') => {
activityId
:
''
,
detailId
:
''
,
rawParams
:
{},
rawScene
:
''
,
}
}
const
querySource
=
extractQuerySource
(
normalized
)
const
rawParams
=
parseQueryString
(
querySource
)
// 兼容 `pages/xxx?scene=detailId,activityId` 这种太阳码 path 场景。
const
sceneParams
=
parseScanCheckinSceneParams
(
rawParams
.
scene
||
''
)
return
{
activityId
:
pickFirstAvailableValue
(
rawParams
,
[
'activity_id'
,
'activityId'
,
'id'
]),
detailId
:
pickFirstAvailableValue
(
rawParams
,
[
'detail_id'
,
'detailId'
,
'stage_id'
,
'stageId'
]),
activityId
:
sceneParams
.
activityId
||
pickFirstAvailableValue
(
rawParams
,
[
'activity_id'
,
'activityId'
,
'id'
]),
detailId
:
sceneParams
.
detailId
||
pickFirstAvailableValue
(
rawParams
,
[
'detail_id'
,
'detailId'
,
'stage_id'
,
'stageId'
]),
rawParams
,
rawScene
:
sceneParams
.
rawScene
,
}
}
/**
* @description 判断当前详情页和扫码结果是否指向同一个打卡点
* @param {{activityId:string|number, detailId:string|number}} currentTarget
* @param {{activityId:string|number, detailId:string|number}} scannedTarget
* @returns {boolean}
*/
export
const
isSameScanCheckinTarget
=
(
currentTarget
=
{},
scannedTarget
=
{})
=>
{
const
currentActivityId
=
normalizeTargetValue
(
currentTarget
.
activityId
)
const
currentDetailId
=
normalizeTargetValue
(
currentTarget
.
detailId
)
const
scannedActivityId
=
normalizeTargetValue
(
scannedTarget
.
activityId
)
const
scannedDetailId
=
normalizeTargetValue
(
scannedTarget
.
detailId
)
if
(
!
currentActivityId
||
!
currentDetailId
||
!
scannedActivityId
||
!
scannedDetailId
)
{
return
false
}
return
currentActivityId
===
scannedActivityId
&&
currentDetailId
===
scannedDetailId
}
const
extractQuerySource
=
input
=>
{
...
...
@@ -98,6 +125,8 @@ const pickFirstAvailableValue = (params, keys = []) => {
return
''
}
const
normalizeTargetValue
=
value
=>
String
(
value
||
''
).
trim
()
const
safeDecodeURIComponent
=
(
value
=
''
)
=>
{
try
{
return
decodeURIComponent
(
String
(
value
||
''
).
replace
(
/
\+
/g
,
'%20'
))
...
...
Please
register
or
login
to post a comment