diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 6c32c5fd..974e4bc4 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -88,5 +88,6 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SelectInstances: typeof import('./src/components/fc/SelectInstances.vue')['default'] + UploadFileDialog: typeof import('./src/components/fc/UploadFileDialog.vue')['default'] } } diff --git a/frontend/src/components/fc/UploadFileDialog.vue b/frontend/src/components/fc/UploadFileDialog.vue new file mode 100644 index 00000000..45d2013e --- /dev/null +++ b/frontend/src/components/fc/UploadFileDialog.vue @@ -0,0 +1,104 @@ + + + + + + + + + {{ percentComplete > 0 ? t("TXT_CODE_b625dbf0") + percentComplete + "%" : t("选择文件") }} + + + {{ t("TXT_CODE_a0451c97") }} + + + + + + diff --git a/frontend/src/components/fc/index.ts b/frontend/src/components/fc/index.ts index ec6f01ac..d269e657 100644 --- a/frontend/src/components/fc/index.ts +++ b/frontend/src/components/fc/index.ts @@ -6,6 +6,7 @@ import CmdAssistantDialog from "@/components/fc/CmdAssistantDialog/index.vue"; import KvOptionsDialogVue from "@/components/fc/KvOptionsDialog.vue"; import { t } from "@/lang/i18n"; import type { AntColumnsType } from "@/types/ant"; +import UploadFileDialogVue from "./UploadFileDialog.vue"; interface DockerConfigItem { host: string; @@ -15,6 +16,10 @@ interface PortConfigItem extends DockerConfigItem { protocol: string; } +export async function useUploadFileDialog() { + return (await useMountComponent().mount(UploadFileDialogVue)) || ""; +} + export async function useSelectInstances() { return await useMountComponent().mount(SelectInstances); } diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts index 5c0e758c..f64a675b 100644 --- a/frontend/src/config/index.ts +++ b/frontend/src/config/index.ts @@ -39,6 +39,7 @@ import TitleCard from "@/widgets/TitleCard.vue"; import LoginCard from "@/widgets/LoginCard.vue"; import DefaultCard from "@/widgets/DefaultCard.vue"; import Carousel from "@/widgets/others/Carousel.vue"; +import PluginCard from "@/widgets/others/PluginCard.vue"; import { NEW_CARD_TYPE } from "../types/index"; import { ROLE } from "./router"; @@ -81,7 +82,8 @@ export const LAYOUT_CARD_TYPES: { [key: string]: any } = { Schedule, InstanceShortcut, DefaultCard, - Carousel + Carousel, + PluginCard }; export interface NewCardItem extends LayoutCard { @@ -471,6 +473,19 @@ export function getLayoutCardPool() { type: "instance" } ] + }, + { + id: getRandomId(), + permission: ROLE.GUEST, + meta: {}, + type: "PluginCard", + title: t("扩展页面卡片"), + width: 6, + description: t( + "此卡片可以上传自定义 HTML 页面并直接执行 Javascript 脚本,可以直接使用网页上所有元素,适用于 Web 前端开发人员。" + ), + height: LayoutCardHeight.MEDIUM, + category: NEW_CARD_TYPE.COMMON } ]; return LAYOUT_CARD_POOL; diff --git a/frontend/src/hooks/useMountComponent.ts b/frontend/src/hooks/useMountComponent.ts index a2e833d6..4cc10df5 100644 --- a/frontend/src/hooks/useMountComponent.ts +++ b/frontend/src/hooks/useMountComponent.ts @@ -1,3 +1,5 @@ +/* eslint-disable vue/one-component-per-file */ + import { createApp, type Component } from "vue"; import { sleep } from "@/tools/common"; @@ -30,3 +32,20 @@ export function useMountComponent(data: Record = {}) { mount }; } + +// export function useFcComponent(component: Component, data: Record = {}): T { +// const div = document.createElement("div"); +// document.body.appendChild(div); +// const app = createApp(component, { +// ...data, +// async destroyComponent(delay = 1000) { +// await sleep(delay); +// app.unmount(); +// div.remove(); +// } +// }); +// console.debug("XZXZ:", component); +// app.mount(div); + +// return app._instance?.exposed as T; +// } diff --git a/frontend/src/services/apiService.ts b/frontend/src/services/apiService.ts index c5fbe3f8..75d7a588 100644 --- a/frontend/src/services/apiService.ts +++ b/frontend/src/services/apiService.ts @@ -37,6 +37,8 @@ class ApiService { private readonly REQUEST_CACHE_TIME = 100; public async subscribe(config: RequestConfig): Promise { + if (!config.url) throw new Error("ApiService: RequestConfig: 'url' is empty!"); + config = _.cloneDeep(config); // filter and clean up expired cache tables this.responseMap.forEach((value, key) => { diff --git a/frontend/src/tools/sandbox.ts b/frontend/src/tools/sandbox.ts new file mode 100644 index 00000000..87239bc4 --- /dev/null +++ b/frontend/src/tools/sandbox.ts @@ -0,0 +1,88 @@ +import axios from "axios"; + +class SandboxBridge { + [key: string | symbol | number]: any; + + private _callbacks = new Map(); + + // API + public $axios = axios; + public $realWindow = window; + + public $onMounted(callback: Function) { + this._addCallback("onMounted", callback); + } + + public $onUnmounted(callback: Function) { + this._addCallback("onUnmounted", callback); + } + + public $emit(event: string, ...args: any[]) { + const callbacks = this._callbacks.get(event); + if (callbacks && callbacks.length > 0) { + callbacks.forEach((callback) => { + try { + callback.call(null, ...args); + } catch (error) { + reportError(error); + } + }); + } + } + + public $destroySandbox() { + this.$emit("onUnmounted"); + // this._callbacks.clear(); + } + + public $mountSandbox() { + this.$emit("onMounted"); + } + + private _addCallback(event: string, callback: Function) { + const callbacks = this._callbacks.get(event) || []; + callbacks.push(callback); + this._callbacks.set(event, callbacks); + } +} + +export class ProxySandBox { + public proxyWindow; + public fakeWindow: SandboxBridge; + + constructor() { + this.fakeWindow = new SandboxBridge(); + this.proxyWindow = new Proxy(this.fakeWindow, { + set: (target, prop, value) => { + target[prop] = value; + return true; + }, + get: (target, prop: any) => { + const attr = prop in target ? target[prop] : window[prop]; + if (typeof attr === "function") { + return attr.bind(prop in target ? target : window); + } + return attr; + } + }); + } + + public executeJavascript(code: string) { + const proxyFunc = Function("window", `"use strict";\n${code}`); + proxyFunc?.call(this.proxyWindow, this.proxyWindow); + } + + public destroy() { + this.fakeWindow.$destroySandbox(); + } + + public mount() { + this.fakeWindow.$mountSandbox(); + } +} + +export function useProxySandbox(code = "") { + const sandbox = new ProxySandBox(); + sandbox.executeJavascript(code); + return sandbox; +} diff --git a/frontend/src/widgets/others/PluginCard.vue b/frontend/src/widgets/others/PluginCard.vue new file mode 100644 index 00000000..b84cbe9c --- /dev/null +++ b/frontend/src/widgets/others/PluginCard.vue @@ -0,0 +1,107 @@ + + + + + + + {{ card.title }} + + + + + + + + {{ t("支持上传 HTML 文件,此卡片会加载 HTML 并运行 Javascript 代码。") }} + + {{ t("使用其他人分享的文件可能会导致面板被入侵。") }} + + {{ t("HTML 文件:") }} + + {{ originUrl }} + + + {{ t("上传 HTML 文件") }} + + + + + + + + + + + +
+ {{ t("支持上传 HTML 文件,此卡片会加载 HTML 并运行 Javascript 代码。") }} + + {{ t("使用其他人分享的文件可能会导致面板被入侵。") }} +
{{ originUrl }}