feat: 新增义工登录和核销功能模块
添加义工登录页面和核销结果页面 实现义工登录、权限校验和票务核销API 更新路由配置和axios拦截器逻辑 添加相关依赖和类型检查配置
Showing
10 changed files
with
541 additions
and
9 deletions
This diff is collapsed. Click to expand it.
| ... | @@ -79,6 +79,7 @@ | ... | @@ -79,6 +79,7 @@ |
| 79 | "vite": "^2.9.9", | 79 | "vite": "^2.9.9", |
| 80 | "vite-plugin-style-import": "1.4.1", | 80 | "vite-plugin-style-import": "1.4.1", |
| 81 | "vue-esign": "^1.1.4", | 81 | "vue-esign": "^1.1.4", |
| 82 | - "vue-router": "^4.0.15" | 82 | + "vue-router": "^4.0.15", |
| 83 | + "vue-tsc": "^1.8.27" | ||
| 83 | } | 84 | } |
| 84 | } | 85 | } | ... | ... |
src/api/redeem.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-01-22 10:02:18 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-01-22 10:09:55 | ||
| 5 | + * @FilePath: /git/xysBooking/src/api/redeem.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +import { fn, fetch } from '@/api/fn'; | ||
| 9 | + | ||
| 10 | +const Api = { | ||
| 11 | + BASE: '/srv/', | ||
| 12 | +}; | ||
| 13 | + | ||
| 14 | +export const volunteerLoginAPI = (params) => { | ||
| 15 | + return fn(fetch.basePost(Api.BASE, params, { params: { f: 'reserve_admin', a: 'login' } })); | ||
| 16 | +}; | ||
| 17 | + | ||
| 18 | +export const checkRedeemPermissionAPI = (params) => { | ||
| 19 | + return fn(fetch.get(Api.BASE, { ...(params || {}), f: 'reserve_admin', a: 'user', t: 'check_auth' })); | ||
| 20 | +}; | ||
| 21 | + | ||
| 22 | +export const verifyTicketAPI = (params) => { | ||
| 23 | + return fn(fetch.basePost(Api.BASE, params, { params: { f: 'reserve_admin', a: 'bill', t: 'redeem' } })); | ||
| 24 | +}; |
src/assets/images/logo_01.png
0 → 100644
6.5 KB
| 1 | /* | 1 | /* |
| 2 | * @Date: 2023-06-13 13:26:46 | 2 | * @Date: 2023-06-13 13:26:46 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2024-02-06 12:05:22 | 4 | + * @LastEditTime: 2026-01-22 10:02:16 |
| 5 | - * @FilePath: /xysBooking/src/route.js | 5 | + * @FilePath: /git/xysBooking/src/route.js |
| 6 | * @Description: 路由列表 | 6 | * @Description: 路由列表 |
| 7 | */ | 7 | */ |
| 8 | export default [ | 8 | export default [ |
| ... | @@ -123,4 +123,18 @@ export default [ | ... | @@ -123,4 +123,18 @@ export default [ |
| 123 | title: '我的', | 123 | title: '我的', |
| 124 | }, | 124 | }, |
| 125 | }, | 125 | }, |
| 126 | + { | ||
| 127 | + path: '/volunteerLogin', | ||
| 128 | + component: () => import('@/views/volunteerLogin.vue'), | ||
| 129 | + meta: { | ||
| 130 | + title: '义工登录', | ||
| 131 | + }, | ||
| 132 | + }, | ||
| 133 | + { | ||
| 134 | + path: '/verificationResult', | ||
| 135 | + component: () => import('@/views/verificationResult.vue'), | ||
| 136 | + meta: { | ||
| 137 | + title: '核销', | ||
| 138 | + }, | ||
| 139 | + }, | ||
| 126 | ]; | 140 | ]; | ... | ... |
| ... | @@ -51,13 +51,13 @@ axios.interceptors.response.use( | ... | @@ -51,13 +51,13 @@ axios.interceptors.response.use( |
| 51 | // // C/B 授权拼接头特殊标识,openid_x | 51 | // // C/B 授权拼接头特殊标识,openid_x |
| 52 | // let prefixAPI = router?.currentRoute.value.href?.indexOf('business') > 0 ? 'b' : 'c'; | 52 | // let prefixAPI = router?.currentRoute.value.href?.indexOf('business') > 0 ? 'b' : 'c'; |
| 53 | if (response.data.code === 401) { | 53 | if (response.data.code === 401) { |
| 54 | - // 特殊标识-带此标识报错不显示 | ||
| 55 | response.data.show = false; | 54 | response.data.show = false; |
| 56 | - /** | 55 | + const request_params = response?.config?.params || {}; |
| 57 | - * 未授权跳转登录页 | 56 | + const is_redeem_admin = request_params?.f === 'reserve_admin'; |
| 58 | - * 带着上一个页面的信息, 授权完成后 返回当前页面 | 57 | + const current_path = router?.currentRoute?.value?.path || ''; |
| 59 | - */ | 58 | + if (!is_redeem_admin && current_path !== '/auth') { |
| 60 | - router.replace({ path: '/auth', query: { href: location.hash } }); | 59 | + router.replace({ path: '/auth', query: { href: location.hash } }); |
| 60 | + } | ||
| 61 | } | 61 | } |
| 62 | if (['预约ID不存在'].includes(response.data.msg)) { | 62 | if (['预约ID不存在'].includes(response.data.msg)) { |
| 63 | response.data.show = false; | 63 | response.data.show = false; | ... | ... |
src/views/verificationResult.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="verify-page"> | ||
| 3 | + <div class="status-card"> | ||
| 4 | + <div class="status-icon" :class="verify_status"> | ||
| 5 | + <van-icon :name="status_icon" size="24" :color="status_icon_color" /> | ||
| 6 | + </div> | ||
| 7 | + <div class="status-text"> | ||
| 8 | + <div class="title">{{ status_title }}</div> | ||
| 9 | + <div class="desc" v-if="verify_status === 'idle'">扫描预约码二维码进行核销</div> | ||
| 10 | + <div class="desc" v-else>{{ msg }}</div> | ||
| 11 | + </div> | ||
| 12 | + </div> | ||
| 13 | + | ||
| 14 | + <div class="info-card"> | ||
| 15 | + <div class="card-title">核销记录信息</div> | ||
| 16 | + | ||
| 17 | + <template v-if="verify_info && Object.keys(verify_info).length > 0"> | ||
| 18 | + <div class="row"> | ||
| 19 | + <div class="label">姓名</div> | ||
| 20 | + <div class="value">{{ verify_info.person_name || '-' }}</div> | ||
| 21 | + </div> | ||
| 22 | + <div class="row"> | ||
| 23 | + <div class="label">证件号码</div> | ||
| 24 | + <div class="value">{{ format_id_number(verify_info.id_number) || '-' }}</div> | ||
| 25 | + </div> | ||
| 26 | + <div class="row"> | ||
| 27 | + <div class="label">状态</div> | ||
| 28 | + <div class="value highlight">{{ verify_info.status || '-' }}</div> | ||
| 29 | + </div> | ||
| 30 | + <div class="row"> | ||
| 31 | + <div class="label">预约开始</div> | ||
| 32 | + <div class="value">{{ verify_info.begin_time || '-' }}</div> | ||
| 33 | + </div> | ||
| 34 | + <div class="row last"> | ||
| 35 | + <div class="label">预约结束</div> | ||
| 36 | + <div class="value">{{ verify_info.end_time || '-' }}</div> | ||
| 37 | + </div> | ||
| 38 | + </template> | ||
| 39 | + | ||
| 40 | + <template v-else-if="verify_status === 'fail'"> | ||
| 41 | + <div class="fail-reason"> | ||
| 42 | + <div class="label">失败原因</div> | ||
| 43 | + <div class="reason">{{ msg }}</div> | ||
| 44 | + </div> | ||
| 45 | + </template> | ||
| 46 | + | ||
| 47 | + <template v-else> | ||
| 48 | + <div class="empty">暂无核销信息</div> | ||
| 49 | + </template> | ||
| 50 | + </div> | ||
| 51 | + | ||
| 52 | + <div class="verify-footer"> | ||
| 53 | + <van-field v-model="manual_code" placeholder="可粘贴/输入预约码(非微信环境备用)" clearable /> | ||
| 54 | + <div class="btn-wrap"> | ||
| 55 | + <van-button | ||
| 56 | + block | ||
| 57 | + color="#A67939" | ||
| 58 | + :loading="verify_status === 'verifying'" | ||
| 59 | + :disabled="verify_status === 'verifying'" | ||
| 60 | + @click="start_scan_and_verify" | ||
| 61 | + > | ||
| 62 | + 核销 | ||
| 63 | + </van-button> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | +</template> | ||
| 68 | + | ||
| 69 | +<script setup> | ||
| 70 | +import { computed, onMounted, ref, watch } from 'vue' | ||
| 71 | +import { useRoute, useRouter } from 'vue-router' | ||
| 72 | +import wx from 'weixin-js-sdk' | ||
| 73 | +import { showToast } from 'vant' | ||
| 74 | +import { apiList } from '@/api/wx/jsApiList' | ||
| 75 | +import { wxJsAPI } from '@/api/wx/config' | ||
| 76 | +import { mainStore } from '@/store' | ||
| 77 | +import { checkRedeemPermissionAPI, verifyTicketAPI } from '@/api/redeem' | ||
| 78 | +import { wxInfo } from '@/utils/tools' | ||
| 79 | + | ||
| 80 | +const store = mainStore() | ||
| 81 | +const $route = useRoute() | ||
| 82 | +const $router = useRouter() | ||
| 83 | + | ||
| 84 | +const manual_code = ref('') | ||
| 85 | +const verify_code = ref('') | ||
| 86 | +const verify_info = ref({}) | ||
| 87 | +const verify_status = ref('idle') | ||
| 88 | +const msg = ref('请点击下方按钮进行核销') | ||
| 89 | + | ||
| 90 | +const status_title = computed(() => { | ||
| 91 | + if (verify_status.value === 'verifying') return '核销中' | ||
| 92 | + if (verify_status.value === 'success') return '核销成功' | ||
| 93 | + if (verify_status.value === 'fail') return '核销失败' | ||
| 94 | + return '核销' | ||
| 95 | +}) | ||
| 96 | + | ||
| 97 | +const status_icon = computed(() => { | ||
| 98 | + if (verify_status.value === 'verifying') return 'clock-o' | ||
| 99 | + if (verify_status.value === 'success') return 'passed' | ||
| 100 | + if (verify_status.value === 'fail') return 'close' | ||
| 101 | + return 'info-o' | ||
| 102 | +}) | ||
| 103 | + | ||
| 104 | +const status_icon_color = computed(() => { | ||
| 105 | + if (verify_status.value === 'fail') return '#E24A4A' | ||
| 106 | + return '#A67939' | ||
| 107 | +}) | ||
| 108 | + | ||
| 109 | +const format_id_number = (id) => { | ||
| 110 | + if (!id || typeof id !== 'string' || id.length < 10) return id | ||
| 111 | + return id.replace(/^(.{6})(?:\d+)(.{4})$/, '$1********$2') | ||
| 112 | +} | ||
| 113 | + | ||
| 114 | +const normalize_scan_result = (raw) => { | ||
| 115 | + if (!raw) return '' | ||
| 116 | + const text = String(raw) | ||
| 117 | + const barcode_split = text.split(',') | ||
| 118 | + const candidate = barcode_split.length > 1 ? barcode_split[barcode_split.length - 1] : text | ||
| 119 | + if (candidate.includes('qr_code=')) { | ||
| 120 | + try { | ||
| 121 | + const url = new URL(candidate) | ||
| 122 | + return url.searchParams.get('qr_code') || candidate | ||
| 123 | + } catch (e) { | ||
| 124 | + const match = candidate.match(/(?:\?|&)qr_code=([^&]+)/) | ||
| 125 | + if (match && match[1]) return decodeURIComponent(match[1]) | ||
| 126 | + } | ||
| 127 | + } | ||
| 128 | + return candidate | ||
| 129 | +} | ||
| 130 | + | ||
| 131 | +const ensure_permission = async () => { | ||
| 132 | + const permission_res = await checkRedeemPermissionAPI() | ||
| 133 | + if (!permission_res || permission_res?.code !== 1) { | ||
| 134 | + $router.replace({ path: '/volunteerLogin' }) | ||
| 135 | + return false | ||
| 136 | + } | ||
| 137 | + if (permission_res?.data) store.changeUserInfo(permission_res.data) | ||
| 138 | + if (permission_res?.data?.can_redeem !== true) { | ||
| 139 | + $router.replace({ path: '/volunteerLogin' }) | ||
| 140 | + return false | ||
| 141 | + } | ||
| 142 | + return true | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +const verify_ticket = async (code) => { | ||
| 146 | + const normalized = normalize_scan_result(code) | ||
| 147 | + if (!normalized) return | ||
| 148 | + if (verify_status.value === 'verifying') return | ||
| 149 | + | ||
| 150 | + verify_code.value = normalized | ||
| 151 | + verify_status.value = 'verifying' | ||
| 152 | + msg.value = '核销中...' | ||
| 153 | + | ||
| 154 | + const res = await verifyTicketAPI({ qr_code: normalized }) | ||
| 155 | + if (res?.code === 1) { | ||
| 156 | + verify_status.value = 'success' | ||
| 157 | + msg.value = res?.msg || '核销成功' | ||
| 158 | + verify_info.value = res?.data || {} | ||
| 159 | + return | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + verify_status.value = 'fail' | ||
| 163 | + msg.value = res?.msg || '核销失败' | ||
| 164 | + verify_info.value = {} | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +const init_wx_scan = async () => { | ||
| 168 | + const cfg_res = await wxJsAPI({ url: window.location.href.split('#')[0] }) | ||
| 169 | + if (!cfg_res || cfg_res?.code !== 1 || !cfg_res?.data) return false | ||
| 170 | + const cfg = { ...cfg_res.data, jsApiList: apiList } | ||
| 171 | + return new Promise((resolve) => { | ||
| 172 | + wx.config(cfg) | ||
| 173 | + wx.ready(() => resolve(true)) | ||
| 174 | + wx.error(() => resolve(false)) | ||
| 175 | + }) | ||
| 176 | +} | ||
| 177 | + | ||
| 178 | +const scan_in_wechat = async () => { | ||
| 179 | + const ok = await init_wx_scan() | ||
| 180 | + if (!ok) return '' | ||
| 181 | + return new Promise((resolve) => { | ||
| 182 | + wx.scanQRCode({ | ||
| 183 | + needResult: 1, | ||
| 184 | + scanType: ['qrCode', 'barCode'], | ||
| 185 | + success: (res) => resolve(res?.resultStr || ''), | ||
| 186 | + fail: () => resolve(''), | ||
| 187 | + cancel: () => resolve(''), | ||
| 188 | + }) | ||
| 189 | + }) | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +const start_scan_and_verify = async () => { | ||
| 193 | + const authed = await ensure_permission() | ||
| 194 | + if (!authed) return | ||
| 195 | + | ||
| 196 | + const in_wechat = wxInfo().isTable === true | ||
| 197 | + if (in_wechat) { | ||
| 198 | + const result = await scan_in_wechat() | ||
| 199 | + const code = normalize_scan_result(result) | ||
| 200 | + if (!code) { | ||
| 201 | + if (manual_code.value) await verify_ticket(manual_code.value) | ||
| 202 | + else showToast('未获取到二维码内容') | ||
| 203 | + return | ||
| 204 | + } | ||
| 205 | + await verify_ticket(code) | ||
| 206 | + return | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + if (manual_code.value) { | ||
| 210 | + await verify_ticket(manual_code.value) | ||
| 211 | + return | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + showToast('请在微信内扫码,或手动输入预约码') | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +onMounted(async () => { | ||
| 218 | + const authed = await ensure_permission() | ||
| 219 | + if (!authed) return | ||
| 220 | + const code = $route.query?.result || $route.query?.qr_code || '' | ||
| 221 | + const str_code = Array.isArray(code) ? code[0] : String(code || '') | ||
| 222 | + if (str_code) { | ||
| 223 | + manual_code.value = str_code | ||
| 224 | + await verify_ticket(str_code) | ||
| 225 | + } | ||
| 226 | +}) | ||
| 227 | + | ||
| 228 | +watch( | ||
| 229 | + () => $route.query?.result, | ||
| 230 | + async (next) => { | ||
| 231 | + const code = Array.isArray(next) ? next[0] : String(next || '') | ||
| 232 | + if (!code) return | ||
| 233 | + if (verify_code.value === code) return | ||
| 234 | + const authed = await ensure_permission() | ||
| 235 | + if (!authed) return | ||
| 236 | + manual_code.value = code | ||
| 237 | + await verify_ticket(code) | ||
| 238 | + } | ||
| 239 | +) | ||
| 240 | +</script> | ||
| 241 | + | ||
| 242 | +<style lang="less" scoped> | ||
| 243 | +.verify-page { | ||
| 244 | + min-height: 100vh; | ||
| 245 | + background-color: #F6F6F6; | ||
| 246 | + padding: 16px; | ||
| 247 | + padding-bottom: 160px; | ||
| 248 | + box-sizing: border-box; | ||
| 249 | + | ||
| 250 | + .status-card { | ||
| 251 | + background: #fff; | ||
| 252 | + border-radius: 12px; | ||
| 253 | + padding: 16px; | ||
| 254 | + display: flex; | ||
| 255 | + align-items: center; | ||
| 256 | + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04); | ||
| 257 | + | ||
| 258 | + .status-icon { | ||
| 259 | + width: 48px; | ||
| 260 | + height: 48px; | ||
| 261 | + border-radius: 50%; | ||
| 262 | + background-color: #FFF7ED; | ||
| 263 | + display: flex; | ||
| 264 | + align-items: center; | ||
| 265 | + justify-content: center; | ||
| 266 | + margin-right: 12px; | ||
| 267 | + } | ||
| 268 | + | ||
| 269 | + .status-text { | ||
| 270 | + flex: 1; | ||
| 271 | + .title { | ||
| 272 | + font-size: 18px; | ||
| 273 | + font-weight: 600; | ||
| 274 | + color: #111827; | ||
| 275 | + } | ||
| 276 | + .desc { | ||
| 277 | + margin-top: 4px; | ||
| 278 | + font-size: 13px; | ||
| 279 | + color: #6B7280; | ||
| 280 | + word-break: break-all; | ||
| 281 | + } | ||
| 282 | + } | ||
| 283 | + } | ||
| 284 | + | ||
| 285 | + .info-card { | ||
| 286 | + margin-top: 12px; | ||
| 287 | + background: #fff; | ||
| 288 | + border-radius: 12px; | ||
| 289 | + padding: 16px; | ||
| 290 | + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.04); | ||
| 291 | + | ||
| 292 | + .card-title { | ||
| 293 | + font-size: 14px; | ||
| 294 | + color: #6B7280; | ||
| 295 | + margin-bottom: 12px; | ||
| 296 | + } | ||
| 297 | + | ||
| 298 | + .row { | ||
| 299 | + display: flex; | ||
| 300 | + align-items: center; | ||
| 301 | + justify-content: space-between; | ||
| 302 | + padding: 10px 0; | ||
| 303 | + border-bottom: 1px solid #F3F4F6; | ||
| 304 | + | ||
| 305 | + .label { | ||
| 306 | + font-size: 14px; | ||
| 307 | + color: #6B7280; | ||
| 308 | + } | ||
| 309 | + .value { | ||
| 310 | + font-size: 16px; | ||
| 311 | + color: #111827; | ||
| 312 | + font-weight: 600; | ||
| 313 | + margin-left: 12px; | ||
| 314 | + word-break: break-all; | ||
| 315 | + text-align: right; | ||
| 316 | + } | ||
| 317 | + .highlight { | ||
| 318 | + color: #A67939; | ||
| 319 | + } | ||
| 320 | + } | ||
| 321 | + | ||
| 322 | + .row.last { | ||
| 323 | + border-bottom: 0; | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + .fail-reason { | ||
| 327 | + .label { | ||
| 328 | + font-size: 14px; | ||
| 329 | + color: #6B7280; | ||
| 330 | + margin-bottom: 8px; | ||
| 331 | + } | ||
| 332 | + .reason { | ||
| 333 | + font-size: 16px; | ||
| 334 | + color: #E24A4A; | ||
| 335 | + font-weight: 600; | ||
| 336 | + word-break: break-all; | ||
| 337 | + } | ||
| 338 | + } | ||
| 339 | + | ||
| 340 | + .empty { | ||
| 341 | + font-size: 14px; | ||
| 342 | + color: #9CA3AF; | ||
| 343 | + padding: 8px 0; | ||
| 344 | + } | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + .verify-footer { | ||
| 348 | + position: fixed; | ||
| 349 | + left: 0; | ||
| 350 | + right: 0; | ||
| 351 | + bottom: 0; | ||
| 352 | + padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); | ||
| 353 | + background-color: #fff; | ||
| 354 | + box-shadow: 0 -10px 18px rgba(0, 0, 0, 0.06); | ||
| 355 | + box-sizing: border-box; | ||
| 356 | + | ||
| 357 | + .btn-wrap { | ||
| 358 | + margin-top: 10px; | ||
| 359 | + } | ||
| 360 | + } | ||
| 361 | +} | ||
| 362 | +</style> |
src/views/volunteerLogin.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="volunteer-login-page"> | ||
| 3 | + <div class="logo-section"> | ||
| 4 | + <img :src="logo" alt="logo" class="logo" /> | ||
| 5 | + <div class="app-name">义工登录</div> | ||
| 6 | + </div> | ||
| 7 | + | ||
| 8 | + <div class="login-card"> | ||
| 9 | + <van-field v-model="username" label="账号" placeholder="请输入账号" clearable /> | ||
| 10 | + <van-field v-model="password" label="密码" type="password" placeholder="请输入密码" clearable /> | ||
| 11 | + | ||
| 12 | + <div class="btn-wrap"> | ||
| 13 | + <van-button block color="#A67939" :loading="loading" :disabled="loading" @click="handle_login"> | ||
| 14 | + 立即登录 | ||
| 15 | + </van-button> | ||
| 16 | + </div> | ||
| 17 | + </div> | ||
| 18 | + </div> | ||
| 19 | +</template> | ||
| 20 | + | ||
| 21 | +<script setup> | ||
| 22 | +import { onMounted, ref } from 'vue' | ||
| 23 | +import { useRouter } from 'vue-router' | ||
| 24 | +import { showToast, showSuccessToast } from 'vant' | ||
| 25 | +import { mainStore } from '@/store' | ||
| 26 | +import { checkRedeemPermissionAPI, volunteerLoginAPI } from '@/api/redeem' | ||
| 27 | +import logo from '@/assets/images/logo_01.png' | ||
| 28 | + | ||
| 29 | +const store = mainStore() | ||
| 30 | +const $router = useRouter() | ||
| 31 | + | ||
| 32 | +const username = ref('') | ||
| 33 | +const password = ref('') | ||
| 34 | +const loading = ref(false) | ||
| 35 | + | ||
| 36 | +const check_permission_and_redirect = async () => { | ||
| 37 | + const permission_res = await checkRedeemPermissionAPI() | ||
| 38 | + if (!permission_res) return | ||
| 39 | + if (permission_res?.data) store.changeUserInfo(permission_res.data) | ||
| 40 | + if (permission_res?.data?.can_redeem === true) { | ||
| 41 | + $router.replace({ path: '/verificationResult' }) | ||
| 42 | + } | ||
| 43 | +} | ||
| 44 | + | ||
| 45 | +onMounted(() => { | ||
| 46 | + check_permission_and_redirect() | ||
| 47 | +}) | ||
| 48 | + | ||
| 49 | +const handle_login = async () => { | ||
| 50 | + if (!username.value || !password.value) { | ||
| 51 | + showToast('请输入账号密码') | ||
| 52 | + return | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + loading.value = true | ||
| 56 | + const login_res = await volunteerLoginAPI({ uuid: username.value, password: password.value }) | ||
| 57 | + loading.value = false | ||
| 58 | + | ||
| 59 | + if (!login_res || login_res?.code !== 1) { | ||
| 60 | + showToast(login_res?.msg || '登录失败') | ||
| 61 | + return | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + const permission_res = await checkRedeemPermissionAPI() | ||
| 65 | + if (!permission_res || permission_res?.code !== 1) { | ||
| 66 | + showToast(permission_res?.msg || '权限校验失败') | ||
| 67 | + return | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + if (permission_res?.data) store.changeUserInfo(permission_res.data) | ||
| 71 | + | ||
| 72 | + if (permission_res?.data?.can_redeem === true) { | ||
| 73 | + showSuccessToast(permission_res?.msg || login_res?.msg || '登录成功') | ||
| 74 | + setTimeout(() => $router.replace({ path: '/verificationResult' }), 800) | ||
| 75 | + return | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + showToast(permission_res?.msg || '暂无核销权限') | ||
| 79 | +} | ||
| 80 | +</script> | ||
| 81 | + | ||
| 82 | +<style lang="less" scoped> | ||
| 83 | +.volunteer-login-page { | ||
| 84 | + min-height: 100vh; | ||
| 85 | + background-color: #F6F6F6; | ||
| 86 | + display: flex; | ||
| 87 | + flex-direction: column; | ||
| 88 | + align-items: center; | ||
| 89 | + padding: 80px 16px 16px; | ||
| 90 | + box-sizing: border-box; | ||
| 91 | + | ||
| 92 | + .logo-section { | ||
| 93 | + display: flex; | ||
| 94 | + flex-direction: column; | ||
| 95 | + align-items: center; | ||
| 96 | + margin-bottom: 20px; | ||
| 97 | + | ||
| 98 | + .logo { | ||
| 99 | + width: 80px; | ||
| 100 | + height: 80px; | ||
| 101 | + border-radius: 50%; | ||
| 102 | + background-color: #fff; | ||
| 103 | + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); | ||
| 104 | + object-fit: contain; | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + .app-name { | ||
| 108 | + margin-top: 12px; | ||
| 109 | + font-size: 20px; | ||
| 110 | + font-weight: 600; | ||
| 111 | + color: #333; | ||
| 112 | + letter-spacing: 2px; | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + .login-card { | ||
| 117 | + width: 100%; | ||
| 118 | + max-width: 420px; | ||
| 119 | + background: #fff; | ||
| 120 | + border-radius: 12px; | ||
| 121 | + padding: 12px; | ||
| 122 | + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.04); | ||
| 123 | + box-sizing: border-box; | ||
| 124 | + | ||
| 125 | + .btn-wrap { | ||
| 126 | + margin-top: 16px; | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | +} | ||
| 130 | +</style> |
| ... | @@ -54,6 +54,7 @@ | ... | @@ -54,6 +54,7 @@ |
| 54 | "jquery", | 54 | "jquery", |
| 55 | ], | 55 | ], |
| 56 | "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。 | 56 | "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。 |
| 57 | + "skipLibCheck": true, | ||
| 57 | 58 | ||
| 58 | /* Source Map Options */ | 59 | /* Source Map Options */ |
| 59 | // "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置 | 60 | // "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置 | ... | ... |
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment