|
|
@@ -2,7 +2,10 @@
|
|
|
<div class="login">
|
|
|
<van-nav-bar title="自然人开票" />
|
|
|
|
|
|
- <div class="login-content">
|
|
|
+ <div v-if="restoringSession" class="login-content">
|
|
|
+ <van-loading vertical>正在恢复登录状态…</van-loading>
|
|
|
+ </div>
|
|
|
+ <div v-else class="login-content">
|
|
|
<div class="text">感谢您参加{{ pubName }}项目,请您登录系统开具发票。</div>
|
|
|
|
|
|
<div class="bold-text">已注册电子税务局</div>
|
|
|
@@ -109,7 +112,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onBeforeMount, onActivated } from 'vue'
|
|
|
+import { ref, reactive, watch, onUnmounted } from 'vue'
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
import { showFailToast, showSuccessToast, showToast, showLoadingToast } from 'vant'
|
|
|
import {
|
|
|
@@ -119,11 +122,7 @@ import {
|
|
|
registerUrlApi,
|
|
|
} from '@/services/modules/login'
|
|
|
import { getStatusApi } from '@/services/modules/invoiceInformation'
|
|
|
-import type {
|
|
|
- SendTtsSmsRequest,
|
|
|
- LoginEtssmsRequest,
|
|
|
- RegisterUrlRequest,
|
|
|
-} from '@/services/modules/login/type.d'
|
|
|
+import type { LoginEtssmsRequest, RegisterUrlRequest } from '@/services/modules/login/type.d'
|
|
|
import { useUserStore } from '@/stores/modules/user'
|
|
|
import { useDebounceFn } from '@/utils/util'
|
|
|
import { REGISTER_URL_KEY } from '@/constants/storage'
|
|
|
@@ -132,6 +131,13 @@ import ModernDialog from '@/components/ModernDialog.vue'
|
|
|
const userStore = useUserStore()
|
|
|
const router = useRouter()
|
|
|
const route = useRoute()
|
|
|
+const restoringSession = ref(false)
|
|
|
+const loggingIn = ref(false)
|
|
|
+let loginAttemptId = 0
|
|
|
+
|
|
|
+// 离开登录页或切换链接后,忽略上一轮请求的结果。
|
|
|
+const isCurrentLogin = (attemptId: number, pushRecordId: string) =>
|
|
|
+ attemptId === loginAttemptId && route.name === 'Login' && userStore.pushRecordId === pushRecordId
|
|
|
|
|
|
const passwordType = ref<'text' | 'password'>('password')
|
|
|
const switchPasswordType = () => {
|
|
|
@@ -146,12 +152,6 @@ const formData = reactive<LoginEtssmsRequest>({
|
|
|
password: '',
|
|
|
})
|
|
|
|
|
|
-const sendEtsSms = reactive<SendTtsSmsRequest>({
|
|
|
- mobile: '',
|
|
|
- pushRecordId: '',
|
|
|
- password: '',
|
|
|
-})
|
|
|
-
|
|
|
/* 限制手机号只能输入数字,最多 30 位 */
|
|
|
const onInputMobile = (val: string) => {
|
|
|
formData.mobile = val.replace(/\D/g, '').slice(0, 30)
|
|
|
@@ -184,7 +184,11 @@ const changePasswordDCancel = () => {
|
|
|
}
|
|
|
/* 发送验证码 */
|
|
|
const sendCode = async () => {
|
|
|
- if (sending.value) return
|
|
|
+ if (sending.value || isCounting.value || loggingIn.value || restoringSession.value) return
|
|
|
+ if (!formData.pushRecordId) {
|
|
|
+ showToast('登录链接存在不完整或者输入错误')
|
|
|
+ return
|
|
|
+ }
|
|
|
if (!/^1\d{10}$/.test(formData.mobile)) {
|
|
|
showToast('请输入正确的手机号')
|
|
|
return
|
|
|
@@ -195,10 +199,11 @@ const sendCode = async () => {
|
|
|
}
|
|
|
sending.value = true
|
|
|
const toast = showLoadingToast({ message: '发送中…', forbidClick: true, duration: 3000 })
|
|
|
- sendEtsSms.mobile = formData.mobile
|
|
|
- sendEtsSms.password = formData.password
|
|
|
+ const attemptId = loginAttemptId
|
|
|
+ const { mobile, password, pushRecordId } = formData
|
|
|
try {
|
|
|
- const res = await sendEtsSmsApi(sendEtsSms)
|
|
|
+ const res = await sendEtsSmsApi({ mobile, password, pushRecordId })
|
|
|
+ if (!isCurrentLogin(attemptId, pushRecordId)) return
|
|
|
if (res.code === 0 && res.data) {
|
|
|
showSuccessToast('发送成功')
|
|
|
startCountDown()
|
|
|
@@ -209,6 +214,7 @@ const sendCode = async () => {
|
|
|
toast.close()
|
|
|
} catch (err: any) {
|
|
|
toast.close()
|
|
|
+ if (!isCurrentLogin(attemptId, pushRecordId)) return
|
|
|
|
|
|
const { code, message } = err
|
|
|
if (code === 401) {
|
|
|
@@ -219,7 +225,7 @@ const sendCode = async () => {
|
|
|
|
|
|
showFailToast(err?.message || '获取验证码失败')
|
|
|
} finally {
|
|
|
- sending.value = false
|
|
|
+ if (isCurrentLogin(attemptId, pushRecordId)) sending.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -245,8 +251,46 @@ const loginCancel = () => {
|
|
|
const loginConfirm = () => {
|
|
|
registerUrl()
|
|
|
}
|
|
|
+
|
|
|
+// 首次登录与缓存恢复使用同一套业务状态判断,避免已提交业务重新进入开票页。
|
|
|
+const continueInvoice = async (pushRecordId: string, attemptId: number) => {
|
|
|
+ const token = userStore.access_token
|
|
|
+ const ready = await getPubName(pushRecordId, attemptId)
|
|
|
+ if (!ready || userStore.access_token !== token) return
|
|
|
+ if (!userStore.hasValidSession()) return
|
|
|
+ const statusRes = await getStatusApi({ pushRecordId })
|
|
|
+ if (
|
|
|
+ !isCurrentLogin(attemptId, pushRecordId) ||
|
|
|
+ userStore.access_token !== token ||
|
|
|
+ !userStore.hasValidSession()
|
|
|
+ )
|
|
|
+ return
|
|
|
+ if (Number(statusRes.code) !== 0 || !statusRes.data) {
|
|
|
+ throw new Error(statusRes.msg || '获取开票状态失败,请重试')
|
|
|
+ }
|
|
|
+ const isSubmitStatusMap = ['PENDING', 'FINISHED', 'RED_INK_ENTRY', 'NOT_APPROVED', 'DISCARD']
|
|
|
+ const isSubmitEventStatusMap = [
|
|
|
+ 'SUBMITTED',
|
|
|
+ 'PENDING_PAYMENT',
|
|
|
+ 'PAID',
|
|
|
+ 'PENDING_FINAL_INVOICING',
|
|
|
+ 'INVOICE_FAILED',
|
|
|
+ 'FINAL_INVOICE_SUBMITTED',
|
|
|
+ 'INVOICE_DOWNLOADABLE',
|
|
|
+ ]
|
|
|
+ const { invoiceStatus, eventStatus } = statusRes.data
|
|
|
+ const submitted =
|
|
|
+ isSubmitStatusMap.includes(invoiceStatus) && isSubmitEventStatusMap.includes(eventStatus)
|
|
|
+ await router.replace({ path: submitted ? '/login-success' : '/invoice-information' })
|
|
|
+}
|
|
|
+
|
|
|
/* 登录逻辑 */
|
|
|
const loginBtn = useDebounceFn(async () => {
|
|
|
+ if (route.name !== 'Login' || loggingIn.value || restoringSession.value || sending.value) return
|
|
|
+ if (!formData.pushRecordId) {
|
|
|
+ showFailToast('登录链接存在不完整或者输入错误')
|
|
|
+ return
|
|
|
+ }
|
|
|
if (!formData.mobile) {
|
|
|
showFailToast('请输入手机号')
|
|
|
return
|
|
|
@@ -263,54 +307,41 @@ const loginBtn = useDebounceFn(async () => {
|
|
|
showFailToast('请勾选阅读并同意')
|
|
|
return
|
|
|
}
|
|
|
+ loggingIn.value = true
|
|
|
+ const attemptId = ++loginAttemptId
|
|
|
const toast = showLoadingToast({
|
|
|
message: '登录中…',
|
|
|
forbidClick: true,
|
|
|
duration: 0,
|
|
|
})
|
|
|
|
|
|
- // 已提交状态映射
|
|
|
- const isSubmitStatusMap = ['PENDING', 'FINISHED', 'RED_INK_ENTRY', 'NOT_APPROVED', 'DISCARD']
|
|
|
- const isSubmitEventStatusMap = [
|
|
|
- 'SUBMITTED',
|
|
|
- 'PENDING_PAYMENT',
|
|
|
- 'PAID',
|
|
|
- 'PENDING_FINAL_INVOICING',
|
|
|
- 'INVOICE_FAILED',
|
|
|
- 'FINAL_INVOICE_SUBMITTED',
|
|
|
- 'INVOICE_DOWNLOADABLE',
|
|
|
- ]
|
|
|
-
|
|
|
+ const pushRecordId = formData.pushRecordId
|
|
|
+ let token = userStore.access_token
|
|
|
try {
|
|
|
- const res: any = await loginEtssmsApi(formData)
|
|
|
+ const res: any = await loginEtssmsApi({ ...formData })
|
|
|
+ if (!isCurrentLogin(attemptId, pushRecordId)) return
|
|
|
if (res?.access_token) {
|
|
|
- userStore.setAccessToken(res.access_token)
|
|
|
- const statusRes = await getStatusApi({ pushRecordId: formData.pushRecordId })
|
|
|
- toast.close()
|
|
|
- // 修改认证下的状态内容,如果已经提交,跳转到成功页
|
|
|
- if (statusRes.code == 0) {
|
|
|
- const { invoiceStatus, eventStatus } = statusRes.data
|
|
|
- const isInvoiceSubmitted = invoiceStatus && isSubmitStatusMap.includes(invoiceStatus)
|
|
|
- const isEventSubmitted = eventStatus && isSubmitEventStatusMap.includes(eventStatus)
|
|
|
- if (isInvoiceSubmitted && isEventSubmitted) {
|
|
|
- return router.replace({
|
|
|
- path: '/login-success',
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
- router.replace({ path: '/invoice-information' })
|
|
|
+ userStore.setAccessToken(res.access_token, res.expires_in)
|
|
|
+ token = userStore.access_token
|
|
|
+ formData.password = ''
|
|
|
+ formData.code = ''
|
|
|
+ resetCountDown()
|
|
|
+ await continueInvoice(pushRecordId, attemptId)
|
|
|
} else if (res.code == 401) {
|
|
|
loginMessage.value = res.msg
|
|
|
loginDialog.value = true
|
|
|
} else if (res.code == 402) {
|
|
|
changePasswordDialog.value = true
|
|
|
+ } else {
|
|
|
+ showFailToast(res.msg || '登录失败,请重试')
|
|
|
}
|
|
|
} catch (err: any) {
|
|
|
+ if (!isCurrentLogin(attemptId, pushRecordId) || userStore.access_token !== token) return
|
|
|
+ userStore.LogOut()
|
|
|
+ showFailToast(err?.message || '登录失败,请重试')
|
|
|
+ } finally {
|
|
|
+ if (attemptId === loginAttemptId) loggingIn.value = false
|
|
|
toast.close()
|
|
|
- const { code, message } = err || {}
|
|
|
- if (code && message) {
|
|
|
- showFailToast(message)
|
|
|
- }
|
|
|
}
|
|
|
}, 800)
|
|
|
|
|
|
@@ -336,23 +367,17 @@ const registerUrl = useDebounceFn(async () => {
|
|
|
// 项目名称
|
|
|
const pubName = ref('')
|
|
|
|
|
|
-const getPubName = async () => {
|
|
|
- try {
|
|
|
- const res = await getPubNameApi({
|
|
|
- pushRecordId: sendEtsSms.pushRecordId,
|
|
|
- })
|
|
|
- if (res.code === 0) {
|
|
|
- pubName.value = res.data.projectName
|
|
|
- registerUrlParams.value.areaId = res.data.cityCode
|
|
|
- userStore.setNeedFaceId(res.data.needFaceId)
|
|
|
- registerUrlParams.value.pushRecordId = sendEtsSms.pushRecordId
|
|
|
- } else {
|
|
|
- codeMessage.value = res.msg
|
|
|
- showDialog.value = true
|
|
|
- }
|
|
|
- } catch (err) {
|
|
|
- console.log('err', err)
|
|
|
+const getPubName = async (pushRecordId: string, attemptId: number) => {
|
|
|
+ const res = await getPubNameApi({ pushRecordId })
|
|
|
+ if (!isCurrentLogin(attemptId, pushRecordId)) return false
|
|
|
+ if (Number(res.code) !== 0 || !res.data) {
|
|
|
+ throw new Error(res.msg || '获取项目信息失败,请重新登录')
|
|
|
}
|
|
|
+ pubName.value = res.data.projectName
|
|
|
+ registerUrlParams.value.areaId = res.data.cityCode
|
|
|
+ userStore.setNeedFaceId(res.data.needFaceId)
|
|
|
+ registerUrlParams.value.pushRecordId = pushRecordId
|
|
|
+ return true
|
|
|
}
|
|
|
/* 协议跳转 */
|
|
|
const toAgreement = (type: string) => {
|
|
|
@@ -362,52 +387,91 @@ const toAgreement = (type: string) => {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+const resetCountDown = () => {
|
|
|
+ clearInterval(timer)
|
|
|
+ timer = undefined
|
|
|
+ isCounting.value = false
|
|
|
+ countDown.value = 60
|
|
|
+}
|
|
|
+
|
|
|
const resetLoginState = () => {
|
|
|
formData.mobile = ''
|
|
|
+ formData.password = ''
|
|
|
formData.code = ''
|
|
|
agree.value = false
|
|
|
- isCounting.value = false
|
|
|
- countDown.value = 60
|
|
|
+ resetCountDown()
|
|
|
sending.value = false
|
|
|
showDialog.value = false
|
|
|
loginDialog.value = false
|
|
|
changePasswordDialog.value = false
|
|
|
|
|
|
- if (timer) {
|
|
|
- clearInterval(timer)
|
|
|
- timer = undefined
|
|
|
- }
|
|
|
+ pubName.value = ''
|
|
|
+ registerUrlParams.value.areaId = ''
|
|
|
+ registerUrlParams.value.pushRecordId = ''
|
|
|
+ registerUrlParams.value.returnUrl = ''
|
|
|
}
|
|
|
|
|
|
-onActivated(() => {
|
|
|
+const initializeLogin = async () => {
|
|
|
+ const attemptId = ++loginAttemptId
|
|
|
+ restoringSession.value = false
|
|
|
+ loggingIn.value = false
|
|
|
+ sending.value = false
|
|
|
+ if (route.name !== 'Login') return
|
|
|
if (route.query.from === 'reject') {
|
|
|
resetLoginState()
|
|
|
- router.replace({
|
|
|
+ return router.replace({
|
|
|
path: route.path,
|
|
|
query: {
|
|
|
- pushRecordId: route.query.pushRecordId,
|
|
|
+ pushRecordId: userStore.pushRecordId,
|
|
|
},
|
|
|
})
|
|
|
}
|
|
|
-})
|
|
|
-
|
|
|
-onBeforeMount(() => {
|
|
|
- const userStore = useUserStore()
|
|
|
- userStore.$reset()
|
|
|
- localStorage.removeItem('user-store')
|
|
|
- registerUrlParams.value.returnUrl = `${location.origin}/h5${route.fullPath}`
|
|
|
- const pushRecordId = route.query.pushRecordId
|
|
|
+ const pushRecordId =
|
|
|
+ route.query.pushRecordId === undefined
|
|
|
+ ? userStore.pushRecordId
|
|
|
+ : typeof route.query.pushRecordId === 'string'
|
|
|
+ ? route.query.pushRecordId
|
|
|
+ : ''
|
|
|
+ if (formData.pushRecordId !== pushRecordId) resetLoginState()
|
|
|
+ userStore.setPushRecordId(pushRecordId)
|
|
|
+ formData.pushRecordId = pushRecordId
|
|
|
if (!pushRecordId) {
|
|
|
- userStore.setPushRecordId('')
|
|
|
- formData.pushRecordId = ''
|
|
|
- sendEtsSms.pushRecordId = ''
|
|
|
+ userStore.LogOut()
|
|
|
showToast('登录链接存在不完整或者输入错误')
|
|
|
return
|
|
|
}
|
|
|
- userStore.setPushRecordId(pushRecordId as string)
|
|
|
- sendEtsSms.pushRecordId = pushRecordId as string
|
|
|
- formData.pushRecordId = pushRecordId as string
|
|
|
- getPubName()
|
|
|
+ registerUrlParams.value.returnUrl = `${location.origin}${import.meta.env.BASE_URL}login?pushRecordId=${encodeURIComponent(pushRecordId)}`
|
|
|
+ restoringSession.value = userStore.hasValidSession()
|
|
|
+ const token = userStore.access_token
|
|
|
+ try {
|
|
|
+ if (restoringSession.value) {
|
|
|
+ await continueInvoice(pushRecordId, attemptId)
|
|
|
+ } else {
|
|
|
+ await getPubName(pushRecordId, attemptId)
|
|
|
+ }
|
|
|
+ } catch (err: any) {
|
|
|
+ if (!isCurrentLogin(attemptId, pushRecordId) || userStore.access_token !== token) return
|
|
|
+ userStore.LogOut()
|
|
|
+ showFailToast(err?.message || '恢复登录状态失败,请重新登录')
|
|
|
+ } finally {
|
|
|
+ if (attemptId === loginAttemptId) restoringSession.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// KeepAlive 下同一路由的新链接、协议返回及失效回跳都需要重新检查。
|
|
|
+watch(
|
|
|
+ () => {
|
|
|
+ return route.name === 'Login' ? route.fullPath : ''
|
|
|
+ },
|
|
|
+ () => {
|
|
|
+ void initializeLogin()
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+)
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ loginAttemptId++
|
|
|
+ clearInterval(timer)
|
|
|
})
|
|
|
</script>
|
|
|
|