feat(verificationResult): 重构核销结果页面并优化核销流程
重构核销结果页面UI,增加状态显示和更好的用户反馈 优化核销流程,移除直接扫码功能改为点击核销按钮触发 添加核销状态管理,包括核销中、成功和失败状态
Showing
3 changed files
with
144 additions
and
89 deletions
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2026-01-08 13:01:20 | 2 | * @Date: 2026-01-08 13:01:20 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-08 13:08:22 | 4 | + * @LastEditTime: 2026-01-14 10:07:56 |
| 5 | * @FilePath: /xyxBooking-weapp/src/pages/verificationResult/index.vue | 5 | * @FilePath: /xyxBooking-weapp/src/pages/verificationResult/index.vue |
| 6 | * @Description: 核销结果页面 | 6 | * @Description: 核销结果页面 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | - <view class="result-page"> | 9 | + <view class="min-h-screen bg-gray-100 p-4 verify-page"> |
| 10 | - <view class="result-content"> | 10 | + <view class="rounded-2xl bg-white p-4 shadow-sm"> |
| 11 | - <icon type="success" size="64" color="#A67939"/> | 11 | + <view class="flex items-center"> |
| 12 | - <view class="msg">{{ msg }}</view> | 12 | + <view class="flex h-11 w-11 items-center justify-center rounded-full bg-amber-50"> |
| 13 | - <view class="info" v-if="resultContent">预约码: {{ resultContent }}</view> | 13 | + <icon :type="status_icon_type" size="24" :color="status_icon_color" /> |
| 14 | - <nut-button color="#A67939" class="back-btn" @tap="continueScan">继续核销</nut-button> | 14 | + </view> |
| 15 | - <!-- <nut-button type="default" class="home-btn" @tap="goHome">返回首页</nut-button> --> | 15 | + <view class="ml-4 flex-1"> |
| 16 | + <view class="text-sm font-semibold text-gray-900">{{ status_title }}</view> | ||
| 17 | + <view class="mt-1 text-xs text-gray-500">{{ msg }}</view> | ||
| 18 | + </view> | ||
| 19 | + </view> | ||
| 20 | + </view> | ||
| 21 | + | ||
| 22 | + <view v-if="result_content" class="mt-4 rounded-2xl bg-white p-5 shadow-sm"> | ||
| 23 | + <view class="text-xs text-gray-500">核销结果</view> | ||
| 24 | + <view class="mt-2 break-all text-sm font-medium text-gray-900">{{ result_content }}</view> | ||
| 25 | + </view> | ||
| 26 | + <view v-else class="mt-4 rounded-2xl bg-white p-5 shadow-sm"> | ||
| 27 | + <view class="text-xs text-gray-500">核销结果</view> | ||
| 28 | + <view class="mt-2 text-sm text-gray-400">暂无核销结果,点击下方核销按钮开始扫码</view> | ||
| 29 | + </view> | ||
| 30 | + | ||
| 31 | + <view class="verify-footer"> | ||
| 32 | + <nut-button | ||
| 33 | + block | ||
| 34 | + color="#A67939" | ||
| 35 | + :loading="verify_status === 'verifying'" | ||
| 36 | + :disabled="verify_status === 'verifying'" | ||
| 37 | + class="verify-btn" | ||
| 38 | + @tap="start_scan_and_verify" | ||
| 39 | + > | ||
| 40 | + 核销 | ||
| 41 | + </nut-button> | ||
| 42 | + <view class="mt-4 text-center text-xs text-gray-400">扫描预约码二维码进行核销</view> | ||
| 16 | </view> | 43 | </view> |
| 17 | </view> | 44 | </view> |
| 18 | </template> | 45 | </template> |
| 19 | 46 | ||
| 20 | <script setup> | 47 | <script setup> |
| 21 | -import { ref } from 'vue' | 48 | +import { ref, computed } from 'vue' |
| 22 | import { useRouter } from '@tarojs/taro' | 49 | import { useRouter } from '@tarojs/taro' |
| 23 | -// import { useGo } from '@/hooks/useGo' | ||
| 24 | import { verifyTicketAPI } from '@/api/index' | 50 | import { verifyTicketAPI } from '@/api/index' |
| 25 | import Taro, { useDidShow } from '@tarojs/taro' | 51 | import Taro, { useDidShow } from '@tarojs/taro' |
| 26 | 52 | ||
| 27 | const router = useRouter() | 53 | const router = useRouter() |
| 28 | -// const go = useGo() | 54 | +const result_content = ref('') |
| 29 | -const resultContent = ref('') | 55 | +const verify_status = ref('idle') |
| 30 | -const msg = ref('核销成功') | 56 | +const msg = ref('请点击下方按钮进行核销') |
| 57 | + | ||
| 58 | +// TODO: 还没有真实字段信息, 如果有需要修改, 这个页面涉及verify_status的功能都要注意 | ||
| 59 | +const status_title = computed(() => { | ||
| 60 | + if (verify_status.value === 'verifying') return '核销中' | ||
| 61 | + if (verify_status.value === 'success') return '核销成功' | ||
| 62 | + if (verify_status.value === 'fail') return '核销失败' | ||
| 63 | + return '核销' | ||
| 64 | +}) | ||
| 65 | + | ||
| 66 | +const status_icon_type = computed(() => { | ||
| 67 | + if (verify_status.value === 'verifying') return 'waiting' | ||
| 68 | + if (verify_status.value === 'success') return 'success' | ||
| 69 | + if (verify_status.value === 'fail') return 'cancel' | ||
| 70 | + return 'info' | ||
| 71 | +}) | ||
| 72 | + | ||
| 73 | +const status_icon_color = computed(() => { | ||
| 74 | + if (verify_status.value === 'fail') return '#E24A4A' | ||
| 75 | + return '#A67939' | ||
| 76 | +}) | ||
| 77 | +/******************* END **********************/ | ||
| 78 | + | ||
| 79 | +const verify_ticket = async (code) => { | ||
| 80 | + if (!code) return | ||
| 81 | + if (verify_status.value === 'verifying') return | ||
| 82 | + | ||
| 83 | + result_content.value = code | ||
| 84 | + verify_status.value = 'verifying' | ||
| 85 | + msg.value = '核销中...' | ||
| 31 | 86 | ||
| 32 | -useDidShow(async () => { | ||
| 33 | - resultContent.value = router.params.result || '' | ||
| 34 | - if (resultContent.value) { | ||
| 35 | Taro.showLoading({ title: '核销中...' }) | 87 | Taro.showLoading({ title: '核销中...' }) |
| 36 | - const res = await verifyTicketAPI({ code: resultContent.value }) | 88 | + try { |
| 37 | - Taro.hideLoading() | 89 | + const res = await verifyTicketAPI({ code }) |
| 38 | - if (res.code === 1) { | 90 | + if (res?.code) { |
| 39 | - msg.value = res.msg || '核销成功' | 91 | + verify_status.value = 'success' |
| 40 | - } else { | 92 | + msg.value = res?.msg || '核销成功' |
| 41 | - msg.value = res.msg || '核销失败' | 93 | + return |
| 94 | + } | ||
| 95 | + verify_status.value = 'fail' | ||
| 96 | + msg.value = res?.msg || '核销失败' | ||
| 42 | Taro.showToast({ title: msg.value, icon: 'none' }) | 97 | Taro.showToast({ title: msg.value, icon: 'none' }) |
| 98 | + } catch (e) { | ||
| 99 | + verify_status.value = 'fail' | ||
| 100 | + msg.value = '核销失败' | ||
| 101 | + Taro.showToast({ title: msg.value, icon: 'none' }) | ||
| 102 | + } finally { | ||
| 103 | + Taro.hideLoading() | ||
| 43 | } | 104 | } |
| 105 | +} | ||
| 106 | + | ||
| 107 | +useDidShow(async () => { | ||
| 108 | + /** | ||
| 109 | + * 现在主要用于:外部渠道如果手动带参进入则自动核销;正常业务流程(从义工登录进入)是不会带参的,会走缺省态 + 点击“核销”扫码。 | ||
| 110 | + */ | ||
| 111 | + const code = router?.params?.result || '' | ||
| 112 | + if (!code) { | ||
| 113 | + result_content.value = '' | ||
| 114 | + verify_status.value = 'idle' | ||
| 115 | + msg.value = '请点击下方按钮进行核销' | ||
| 116 | + return | ||
| 44 | } | 117 | } |
| 118 | + await verify_ticket(code) | ||
| 45 | }) | 119 | }) |
| 46 | 120 | ||
| 47 | -const continueScan = () => { | 121 | +const start_scan_and_verify = () => { |
| 48 | Taro.scanCode({ | 122 | Taro.scanCode({ |
| 49 | success: (res) => { | 123 | success: (res) => { |
| 50 | - // Reload current page with new result | 124 | + verify_ticket(res?.result || '') |
| 51 | - // Taro doesn't support easy reload with params change in same page easily without redirect | ||
| 52 | - // So we redirect to self | ||
| 53 | - Taro.redirectTo({ | ||
| 54 | - url: `/pages/verificationResult/index?result=${res.result}` | ||
| 55 | - }) | ||
| 56 | }, | 125 | }, |
| 57 | fail: () => { | 126 | fail: () => { |
| 58 | - // Cancelled | ||
| 59 | } | 127 | } |
| 60 | }) | 128 | }) |
| 61 | } | 129 | } |
| 62 | - | ||
| 63 | -// const goHome = () => { | ||
| 64 | -// Taro.reLaunch({ url: '/pages/index/index' }) | ||
| 65 | -// } | ||
| 66 | </script> | 130 | </script> |
| 67 | 131 | ||
| 68 | <style lang="less"> | 132 | <style lang="less"> |
| 69 | -.result-page { | 133 | +.verify-page { |
| 70 | - padding: 64rpx 32rpx; | 134 | + padding-bottom: 220rpx; |
| 71 | - text-align: center; | 135 | +} |
| 72 | - min-height: 100vh; | ||
| 73 | - background-color: #fff; | ||
| 74 | 136 | ||
| 75 | - .result-content { | 137 | +.verify-footer { |
| 76 | - display: flex; | 138 | + position: fixed; |
| 77 | - flex-direction: column; | 139 | + left: 0; |
| 78 | - align-items: center; | 140 | + right: 0; |
| 79 | - } | 141 | + bottom: 0; |
| 142 | + width: 750rpx; | ||
| 143 | + padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); | ||
| 144 | + background-color: #FFFFFF; | ||
| 145 | + box-sizing: border-box; | ||
| 146 | + box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.08); | ||
| 147 | +} | ||
| 80 | 148 | ||
| 81 | - .msg { | 149 | +.verify-btn { |
| 82 | - font-size: 40rpx; | 150 | + font-size: 36rpx; |
| 83 | - margin-top: 32rpx; | 151 | + height: 104rpx; |
| 84 | - font-weight: bold; | 152 | + line-height: 104rpx; |
| 85 | - color: #333; | 153 | + border-radius: 16rpx; |
| 86 | - } | 154 | + font-weight: 600; |
| 87 | - .info { | ||
| 88 | - margin-top: 20rpx; | ||
| 89 | - color: #999; | ||
| 90 | - font-size: 28rpx; | ||
| 91 | - word-break: break-all; | ||
| 92 | - padding: 0 32rpx; | ||
| 93 | - } | ||
| 94 | - .back-btn { | ||
| 95 | - margin-top: 80rpx; | ||
| 96 | - background-color: #A67939; | ||
| 97 | - color: #fff; | ||
| 98 | - width: 80%; | ||
| 99 | - border-radius: 44rpx; | ||
| 100 | - } | ||
| 101 | - .home-btn { | ||
| 102 | - margin-top: 32rpx; | ||
| 103 | - background-color: #fff; | ||
| 104 | - color: #666; | ||
| 105 | - width: 80%; | ||
| 106 | - border-radius: 44rpx; | ||
| 107 | - border: 1rpx solid #b0b0b0; | ||
| 108 | - } | ||
| 109 | } | 155 | } |
| 110 | </style> | 156 | </style> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2026-01-08 13:01:56 | 2 | * @Date: 2026-01-08 13:01:56 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-13 22:11:30 | 4 | + * @LastEditTime: 2026-01-14 09:59:46 |
| 5 | * @FilePath: /xyxBooking-weapp/src/pages/volunteerLogin/index.vue | 5 | * @FilePath: /xyxBooking-weapp/src/pages/volunteerLogin/index.vue |
| 6 | * @Description: 义工登录页面 | 6 | * @Description: 义工登录页面 |
| 7 | --> | 7 | --> |
| ... | @@ -43,17 +43,37 @@ | ... | @@ -43,17 +43,37 @@ |
| 43 | 43 | ||
| 44 | <script setup> | 44 | <script setup> |
| 45 | import { ref } from 'vue' | 45 | import { ref } from 'vue' |
| 46 | -import Taro from '@tarojs/taro' | 46 | +import Taro, { useDidShow } from '@tarojs/taro' |
| 47 | import { mainStore } from '@/stores/main' | 47 | import { mainStore } from '@/stores/main' |
| 48 | import { volunteerLoginAPI, getUserInfoAPI } from '@/api/index' | 48 | import { volunteerLoginAPI, getUserInfoAPI } from '@/api/index' |
| 49 | -import { useGo } from '@/hooks/useGo' | 49 | +import { useReplace } from '@/hooks/useGo' |
| 50 | import logo from '@/assets/images/logo.png' | 50 | import logo from '@/assets/images/logo.png' |
| 51 | 51 | ||
| 52 | const store = mainStore() | 52 | const store = mainStore() |
| 53 | -const go = useGo() | 53 | +const replace = useReplace() |
| 54 | const username = ref('') | 54 | const username = ref('') |
| 55 | const password = ref('') | 55 | const password = ref('') |
| 56 | 56 | ||
| 57 | +/** | ||
| 58 | + * @description: 检查用户权限并根据角色重定向 | ||
| 59 | + */ | ||
| 60 | + | ||
| 61 | +const check_permission_and_redirect = async () => { | ||
| 62 | + try { | ||
| 63 | + const user_res = await getUserInfoAPI() | ||
| 64 | + if (user_res?.code === 1 && user_res?.data) { | ||
| 65 | + store.changeUserInfo(user_res.data) | ||
| 66 | + if (store.isVolunteer) { | ||
| 67 | + replace('verificationResult') | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + } catch (e) {} | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +useDidShow(() => { | ||
| 74 | + check_permission_and_redirect() | ||
| 75 | +}) | ||
| 76 | + | ||
| 57 | const handleLogin = async () => { | 77 | const handleLogin = async () => { |
| 58 | if (!username.value || !password.value) { | 78 | if (!username.value || !password.value) { |
| 59 | Taro.showToast({ title: '请输入账号密码', icon: 'none' }) | 79 | Taro.showToast({ title: '请输入账号密码', icon: 'none' }) |
| ... | @@ -77,7 +97,7 @@ const handleLogin = async () => { | ... | @@ -77,7 +97,7 @@ const handleLogin = async () => { |
| 77 | if (store.isVolunteer) { | 97 | if (store.isVolunteer) { |
| 78 | Taro.showToast({ title: '登录成功', icon: 'success' }) | 98 | Taro.showToast({ title: '登录成功', icon: 'success' }) |
| 79 | setTimeout(() => { | 99 | setTimeout(() => { |
| 80 | - triggerScan() | 100 | + replace('verificationResult') |
| 81 | }, 1500) | 101 | }, 1500) |
| 82 | } else { | 102 | } else { |
| 83 | Taro.showToast({ title: '非义工账号', icon: 'none' }) | 103 | Taro.showToast({ title: '非义工账号', icon: 'none' }) |
| ... | @@ -90,17 +110,6 @@ const handleLogin = async () => { | ... | @@ -90,17 +110,6 @@ const handleLogin = async () => { |
| 90 | Taro.showToast({ title: loginRes.msg || '登录失败', icon: 'none' }) | 110 | Taro.showToast({ title: loginRes.msg || '登录失败', icon: 'none' }) |
| 91 | } | 111 | } |
| 92 | } | 112 | } |
| 93 | - | ||
| 94 | -const triggerScan = () => { | ||
| 95 | - Taro.scanCode({ | ||
| 96 | - success: (res) => { | ||
| 97 | - go(`/pages/verificationResult/index?result=${res.result}`) | ||
| 98 | - }, | ||
| 99 | - fail: (err) => { | ||
| 100 | - console.log('Scan failed', err) | ||
| 101 | - } | ||
| 102 | - }) | ||
| 103 | -} | ||
| 104 | </script> | 113 | </script> |
| 105 | 114 | ||
| 106 | <style lang="less"> | 115 | <style lang="less"> | ... | ... |
-
Please register or login to post a comment