modify: webview插件入口

This commit is contained in:
2025-09-06 23:04:06 +08:00
parent e9f0c8ef5f
commit 3631b383d6
19 changed files with 996 additions and 498 deletions
+3
View File
@@ -24,3 +24,6 @@ cache/
# nuitka build files
nuitka-crash-report.xml
# plugin files
plugins/
-90
View File
@@ -1,90 +0,0 @@
from PySide6.QtCore import QObject, Slot, Signal, QTimer
from PySide6.QtWebChannel import QWebChannel
import time
class WebviewBridge(QObject):
"""Webview桥接类,用于处理前端和Python后端之间的通信"""
# 定义信号
time_updated = Signal(str)
message_received = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.frontend_ready = False
self.timer = QTimer()
self.timer.timeout.connect(self.send_time)
@Slot()
def on_frontend_ready(self):
"""当前端准备就绪时调用"""
print("Frontend is ready")
self.frontend_ready = True
# 启动定时器,每秒更新一次时间
self.timer.start(1000)
@Slot(str, result=str)
def hello(self, message):
"""处理前端发送的消息"""
print('Message received from frontend:', message)
response = f'Hello from Python: {message}'
self.message_received.emit(response)
return response
def send_time(self):
"""定期向前端发送时间更新"""
if self.frontend_ready:
current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
self.time_updated.emit(current_time)
return current_time
return None
def set_frontend_ready(self, ready=True):
"""设置前端准备状态"""
self.frontend_ready = ready
class WebviewController(QObject):
"""Webview控制器类,负责管理Webview的逻辑"""
def __init__(self, web_engine_page, parent=None):
super().__init__(parent)
self.page = web_engine_page
self.bridge = WebviewBridge()
self.channel = QWebChannel()
self.channel.registerObject('py', self.bridge)
self.page.setWebChannel(self.channel)
# 连接bridge的信号
self.bridge.time_updated.connect(self.on_time_updated)
self.bridge.message_received.connect(self.on_message_received)
def on_time_updated(self, time_str):
"""当bridge发出time_updated信号时调用"""
print(f"Time updated: {time_str}")
# 调用前端的update_time函数
js_code = f"""
try {{
if (typeof update_time !== 'undefined' && typeof update_time === 'function') {{
update_time('{time_str}');
}}
}} catch (e) {{
console.error('Error calling update_time:', e);
}}
"""
self.page.runJavaScript(js_code)
def on_message_received(self, message):
"""当bridge发出message_received信号时调用"""
print(f"Message received: {message}")
# 调用前端的消息处理函数
js_code = f"""
try {{
if (typeof app_sendMessage !== 'undefined' && typeof app_sendMessage === 'function') {{
app_sendMessage('{message}');
}}
}} catch (e) {{
console.error('Error calling app_sendMessage:', e);
}}
"""
self.page.runJavaScript(js_code)
+186
View File
@@ -0,0 +1,186 @@
from PySide6.QtCore import QObject, Slot, QUrl
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtWebEngineCore import QWebEngineSettings
import time
import os
import zipfile
from pathlib import Path
import json
import shutil
import base64
import tempfile
class WebviewBridge(QObject):
"""Webview桥接类,用于处理前端和Python后端之间的通信"""
def __init__(self, webview, parent=None):
super().__init__(parent)
self.webview = webview
# 启用本地文件访问
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowGeolocationOnInsecureOrigins, True)
# 设置Web安全策略
self.webview.page().settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
self.webview.page().settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
self.webview.page().settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True)
# 创建插件目录
self.plugins_dir = Path(os.getcwd()) / "plugins"
self.plugins_dir.mkdir(exist_ok=True)
# 插件信息文件
self.plugins_info_file = self.plugins_dir / "plugins.json"
if not self.plugins_info_file.exists():
with open(self.plugins_info_file, 'w') as f:
json.dump([], f)
@Slot(str, str, str, result=str)
def upload_plugin(self, name, description, file_content):
"""处理插件上传请求"""
try:
# 将base64编码的文件内容解码并保存为临时文件
file_data = base64.b64decode(file_content)
# 创建临时文件来保存上传的ZIP文件
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_file:
tmp_file.write(file_data)
tmp_file_path = tmp_file.name
# 解压插件
success, error = self.extract_plugin(tmp_file_path, name)
if not success:
# 清理临时文件
os.unlink(tmp_file_path)
return json.dumps({"success": False, "error": error})
plugin_info = {
"id": int(time.time()), # 使用时间戳作为简单ID
"name": name,
"description": description,
"version": "1.0.0",
"uploadDate": time.strftime('%Y-%m-%d', time.localtime()),
"path": str(self.plugins_dir / name)
}
# 读取现有插件信息
if self.plugins_info_file.exists():
with open(self.plugins_info_file, 'r') as f:
plugins = json.load(f)
else:
plugins = []
# 添加新插件
plugins.append(plugin_info)
# 保存插件信息
with open(self.plugins_info_file, 'w') as f:
json.dump(plugins, f)
# 清理临时文件
os.unlink(tmp_file_path)
return json.dumps({"success": True})
except Exception as e:
# 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)})
# 解压插件
def extract_plugin(self, plugin_file, plugin_name):
"""解压插件"""
try:
with zipfile.ZipFile(plugin_file, 'r') as zf:
# 获取插件名称
plugin_dir = self.plugins_dir / plugin_name
# 如果插件目录已存在,删除
if plugin_dir.exists():
shutil.rmtree(plugin_dir)
# 创建插件目录并解压
plugin_dir.mkdir(exist_ok=True)
zf.extractall(plugin_dir)
# 检测是否有 index.html 文件
index_file = plugin_dir / "index.html"
if not index_file.exists():
# 查找任何HTML文件作为入口
html_files = list(plugin_dir.glob("*.html"))
if not html_files:
return False, "插件中未找到HTML文件"
index_file = html_files[0]
return True, ""
except Exception as e:
return False, str(e)
@Slot(result=str)
def list_plugins(self):
"""获取插件列表"""
try:
if self.plugins_info_file.exists():
with open(self.plugins_info_file, 'r') as f:
plugins = json.load(f)
return json.dumps({"success": True, "plugins": plugins})
else:
return json.dumps({"success": True, "plugins": []})
except Exception as e:
# 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)})
@Slot(int, result=str)
def launch_plugin(self, plugin_id):
"""启动插件"""
try:
if self.plugins_info_file.exists():
with open(self.plugins_info_file, 'r') as f:
plugins = json.load(f)
# 查找插件
plugin = next((p for p in plugins if p["id"] == plugin_id), None)
if plugin:
# 构建插件URL
plugin_path = Path(plugin["path"])
# 查找HTML文件作为入口
html_files = list(plugin_path.glob("*.html"))
if html_files:
index_file = html_files[0]
url = QUrl.fromLocalFile(str(index_file.absolute()))
print(f"Launching plugin: {url.toString()}")
self.webview.load(url)
return json.dumps({"success": True, "url": url.toString()})
else:
return json.dumps({"success": False, "error": "插件入口文件未找到"})
else:
return json.dumps({"success": False, "error": "插件未找到"})
else:
return json.dumps({"success": False, "error": "插件信息文件未找到"})
except Exception as e:
# 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)})
@Slot(str, result=str)
def save_settings(self, settings_json):
"""保存设置"""
try:
settings = json.loads(settings_json)
# 在实际应用中,这里会保存设置到配置文件
print(f"保存设置: {settings}")
return json.dumps({"success": True})
except Exception as e:
# 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)})
class WebviewController(QObject):
"""Webview控制器类,负责管理Webview的逻辑"""
def __init__(self, web_engine_page, web_webview, parent=None):
super().__init__(parent)
self.page = web_engine_page
self.bridge = WebviewBridge(web_webview)
self.channel = QWebChannel()
self.channel.registerObject('py', self.bridge)
self.page.setWebChannel(self.channel)
+101 -4
View File
@@ -1,12 +1,13 @@
# coding:utf-8
import sys
import os
from pathlib import Path
from PySide6.QtWidgets import QFrame, QVBoxLayout, QMenu, QApplication, QWidget, QPushButton, QHBoxLayout
from qframelesswindow.webengine import FramelessWebEngineView
from PySide6.QtCore import QPoint, Qt, QUrl
from ...config import config
from ...signal.test_webview import WebviewController
from .entrance import WebviewController
class DevToolsWindow(FramelessWebEngineView):
@@ -32,14 +33,18 @@ class WebViewInterface(QFrame):
self.dev_tools_view = None
# 添加连接状态标志
self.is_local_connection = config.get("project", {}).get("is-production", True)
# 插件目录
self.plugins_dir = Path(os.getcwd()) / "plugins"
self.plugins_dir.mkdir(exist_ok=True)
self.webView = FramelessWebEngineView(self)
self.html_path = os.getcwd().replace("\\", '/') + '/resources/webview/test_webview/dist/index.html'
self.html_path = os.getcwd().replace("\\", '/') + '/resources/webview/entrance/dist/index.html'
self.remote_url = "http://localhost:8080/"
# 创建WebviewController实例
self.page = self.webView.page()
self.controller = WebviewController(self.page)
self.controller = WebviewController(self.page, self.webView)
# 加载初始页面
self.load_current_page()
@@ -192,6 +197,8 @@ class WebViewInterface(QFrame):
menu.addSeparator()
menu.addSeparator()
# 在鼠标位置显示菜单
menu.exec(self.webView.mapToGlobal(pos))
@@ -211,10 +218,100 @@ class WebViewInterface(QFrame):
self.dev_tools_view.dev_tools_window.show()
self.dev_tools_view.dev_tools_window.raise_() # 将窗口置于最前
def generate_plugins_page(self):
"""生成插件列表页面"""
plugins = []
for item in self.plugins_dir.iterdir():
if item.is_dir():
index_files = list(item.glob("*.html"))
if index_files:
plugins.append({
'name': item.name,
'path': f"file:///{item.absolute()}/{index_files[0].name}",
'description': f"插件 {item.name}"
})
# 生成HTML页面
html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>插件管理</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.plugins-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.plugin-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.plugin-card h3 {
margin-top: 0;
color: #333;
}
.plugin-card p {
color: #666;
}
.plugin-card a {
display: inline-block;
margin-top: 10px;
padding: 8px 16px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
}
.plugin-card a:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="header">
<h1>插件管理</h1>
<p>当前可用插件列表</p>
</div>
<div class="plugins-grid">
"""
if not plugins:
html += '<p>暂无插件,请上传插件。</p>'
else:
for plugin in plugins:
html += f"""
<div class="plugin-card">
<h3>{plugin['name']}</h3>
<p>{plugin['description']}</p>
<a href="{plugin['path']}" target="_blank">访问插件</a>
</div>
"""
html += """
</div>
</body>
</html>
"""
return html
def closeEvent(self, event):
"""关闭时清理资源"""
# 停止controller中的定时器
self.controller.bridge.timer.stop()
# self.controller.bridge.timer.stop()
if self.dev_tools_view:
self.dev_tools_view.close()
super().closeEvent(event)
@@ -1,4 +1,4 @@
# test_webview
# entrance
## Project setup
```
@@ -1,11 +1,11 @@
{
"name": "test_webview",
"name": "entrance",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "test_webview",
"name": "entrance",
"version": "0.1.0",
"dependencies": {
"core-js": "^3.8.3",
@@ -37,20 +37,6 @@
"node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -67,9 +53,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.28.0",
"resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.0.tgz",
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.4.tgz",
"integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -77,22 +63,22 @@
}
},
"node_modules/@babel/core": {
"version": "7.28.3",
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.3.tgz",
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.3",
"@babel/parser": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -108,9 +94,9 @@
}
},
"node_modules/@babel/eslint-parser": {
"version": "7.28.0",
"resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
"integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.28.4.tgz",
"integrity": "sha512-Aa+yDiH87980jR6zvRfFuCR1+dLb00vBydhTL+zI992Rz/wQhSvuxjmOOuJOgO3XmakO6RykRGD2S1mq1AtgHA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -403,14 +389,14 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.28.3",
"resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.3.tgz",
"integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.2"
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -501,12 +487,12 @@
}
},
"node_modules/@babel/parser": {
"version": "7.28.3",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.3.tgz",
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
"@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -811,9 +797,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
"version": "7.28.0",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
"integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz",
"integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -861,9 +847,9 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
"version": "7.28.3",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz",
"integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
"integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -872,7 +858,7 @@
"@babel/helper-globals": "^7.28.0",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1",
"@babel/traverse": "^7.28.3"
"@babel/traverse": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -1265,9 +1251,9 @@
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.28.0",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
"integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz",
"integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1275,7 +1261,7 @@
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-parameters": "^7.27.7",
"@babel/traverse": "^7.28.0"
"@babel/traverse": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -1402,9 +1388,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.28.3",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz",
"integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz",
"integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1720,9 +1706,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.28.3",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1745,18 +1731,18 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.28.3",
"resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.3.tgz",
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.4.tgz",
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.3",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.2",
"@babel/types": "^7.28.4",
"debug": "^4.3.1"
},
"engines": {
@@ -1764,9 +1750,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.2",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -1869,6 +1855,17 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -2198,9 +2195,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.3.0",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.3.0.tgz",
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
"version": "24.3.1",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.3.1.tgz",
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2783,53 +2780,53 @@
"license": "ISC"
},
"node_modules/@vue/compiler-core": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.20.tgz",
"integrity": "sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
"integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
"@vue/shared": "3.5.20",
"@vue/shared": "3.5.21",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz",
"integrity": "sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz",
"integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.20",
"@vue/shared": "3.5.20"
"@vue/compiler-core": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz",
"integrity": "sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
"integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
"@vue/compiler-core": "3.5.20",
"@vue/compiler-dom": "3.5.20",
"@vue/compiler-ssr": "3.5.20",
"@vue/shared": "3.5.20",
"@vue/compiler-core": "3.5.21",
"@vue/compiler-dom": "3.5.21",
"@vue/compiler-ssr": "3.5.21",
"@vue/shared": "3.5.21",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.17",
"magic-string": "^0.30.18",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz",
"integrity": "sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz",
"integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.20",
"@vue/shared": "3.5.20"
"@vue/compiler-dom": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/component-compiler-utils": {
@@ -2903,53 +2900,53 @@
"license": "ISC"
},
"node_modules/@vue/reactivity": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.20.tgz",
"integrity": "sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.21.tgz",
"integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.20"
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.20.tgz",
"integrity": "sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.21.tgz",
"integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.20",
"@vue/shared": "3.5.20"
"@vue/reactivity": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz",
"integrity": "sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz",
"integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.20",
"@vue/runtime-core": "3.5.20",
"@vue/shared": "3.5.20",
"@vue/reactivity": "3.5.21",
"@vue/runtime-core": "3.5.21",
"@vue/shared": "3.5.21",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.20.tgz",
"integrity": "sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.21.tgz",
"integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.20",
"@vue/shared": "3.5.20"
"@vue/compiler-ssr": "3.5.21",
"@vue/shared": "3.5.21"
},
"peerDependencies": {
"vue": "3.5.20"
"vue": "3.5.21"
}
},
"node_modules/@vue/shared": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.20.tgz",
"integrity": "sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.21.tgz",
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==",
"license": "MIT"
},
"node_modules/@vue/vue-loader-v15": {
@@ -3934,9 +3931,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001737",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz",
"integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==",
"version": "1.0.30001741",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
"integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
"dev": true,
"funding": [
{
@@ -5229,9 +5226,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.211",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz",
"integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==",
"version": "1.5.214",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz",
"integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==",
"dev": true,
"license": "ISC"
},
@@ -8243,9 +8240,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"version": "2.0.20",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.20.tgz",
"integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==",
"dev": true,
"license": "MIT"
},
@@ -10788,14 +10785,14 @@
}
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmmirror.com/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"version": "5.44.0",
"resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.0.tgz",
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@@ -11259,16 +11256,16 @@
}
},
"node_modules/vue": {
"version": "3.5.20",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.20.tgz",
"integrity": "sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==",
"version": "3.5.21",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz",
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.20",
"@vue/compiler-sfc": "3.5.20",
"@vue/runtime-dom": "3.5.20",
"@vue/server-renderer": "3.5.20",
"@vue/shared": "3.5.20"
"@vue/compiler-dom": "3.5.21",
"@vue/compiler-sfc": "3.5.21",
"@vue/runtime-dom": "3.5.21",
"@vue/server-renderer": "3.5.21",
"@vue/shared": "3.5.21"
},
"peerDependencies": {
"typescript": "*"
@@ -1,5 +1,5 @@
{
"name": "test_webview",
"name": "entrance",
"version": "0.1.0",
"private": true,
"scripts": {

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+574
View File
@@ -0,0 +1,574 @@
<template>
<div id="app">
<div class="container">
<h1>插件门户</h1>
<nav class="navbar">
<button @click="showUploadForm" :class="{ active: currentView === 'upload' }">上传插件</button>
<button @click="listPlugins" :class="{ active: currentView === 'list' }">插件列表</button>
<button @click="showSettings" :class="{ active: currentView === 'settings' }">设置</button>
</nav>
<div class="content">
<!-- 上传插件表单 -->
<div v-if="currentView === 'upload'" class="upload-section">
<h2>上传新插件</h2>
<form @submit.prevent="uploadPlugin">
<div class="form-group">
<label for="pluginName">插件名称:</label>
<input type="text" id="pluginName" v-model="pluginForm.name" required>
</div>
<div class="form-group">
<label for="pluginFile">选择插件文件 (ZIP格式):</label>
<input type="file" id="pluginFile" @change="handleFileUpload" accept=".zip" required>
</div>
<div class="form-group">
<label for="pluginDescription">插件描述:</label>
<textarea id="pluginDescription" v-model="pluginForm.description"></textarea>
</div>
<button type="submit" :disabled="uploading">
{{ uploading ? '上传中...' : '上传插件' }}
</button>
</form>
<div v-if="uploadMessage" class="message" :class="uploadMessage.type">
{{ uploadMessage.text }}
</div>
</div>
<!-- 插件列表 -->
<div v-if="currentView === 'list'" class="plugins-section">
<h2>可用插件</h2>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="plugins.length === 0" class="no-plugins">
暂无插件,请先上传插件。
</div>
<div v-else class="plugins-grid">
<div
v-for="plugin in plugins"
:key="plugin.id"
class="plugin-card"
@click="launchPlugin(plugin)"
>
<h3>{{ plugin.name }}</h3>
<p>{{ plugin.description || '无描述' }}</p>
<div class="plugin-meta">
<span class="version">版本: {{ plugin.version || '1.0.0' }}</span>
<span class="date">{{ plugin.uploadDate }}</span>
</div>
</div>
</div>
</div>
<!-- 设置界面 -->
<div v-if="currentView === 'settings'" class="settings-section">
<h2>系统设置</h2>
<div class="form-group">
<label for="theme">主题:</label>
<select id="theme" v-model="settings.theme">
<option value="light">明亮</option>
<option value="dark">暗黑</option>
</select>
</div>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="settings.autoUpdate"
> 自动检查更新
</label>
</div>
<button @click="saveSettings">保存设置</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { QWebChannel } from 'qwebchannel';
export default {
name: 'App',
setup() {
const currentView = ref('upload');
const pluginForm = ref({
name: '',
description: ''
});
const selectedFile = ref(null);
const uploading = ref(false);
const uploadMessage = ref(null);
const plugins = ref([]);
const loading = ref(false);
const settings = ref({
theme: 'light',
autoUpdate: true
});
let py = null;
// 初始化QWebChannel
onMounted(() => {
if (window.qt && window.qt.webChannelTransport) {
new QWebChannel(window.qt.webChannelTransport, function (channel) {
py = channel.objects.py;
// 通知Python端前端已经准备就绪
setTimeout(() => {
if (py && py.on_frontend_ready) {
py.on_frontend_ready();
}
}, 100);
});
}
});
const handleFileUpload = (event) => {
selectedFile.value = event.target.files[0];
};
const uploadPlugin = async () => {
if (!pluginForm.value.name || !selectedFile.value) {
showMessage('请填写插件名称并选择文件', 'error');
return;
}
uploading.value = true;
try {
if (py && py.upload_plugin) {
// 读取文件内容并转换为base64
const reader = new FileReader();
reader.onload = function(e) {
const fileContent = e.target.result.split(',')[1]; // 去掉data:application/zip;base64,前缀
// 调用Python端的上传函数,传递文件名、描述和文件内容
py.upload_plugin(
pluginForm.value.name,
pluginForm.value.description,
fileContent,
(result) => {
try {
// 检查result是否为空或无效
if (!result) {
showMessage('上传失败: 服务器返回空结果', 'error');
uploading.value = false;
return;
}
// 解析从Python返回的JSON字符串
const parsedResult = JSON.parse(result);
if (parsedResult.success) {
showMessage('插件上传成功', 'success');
pluginForm.value = { name: '', description: '' };
// 重置文件输入
document.getElementById('pluginFile').value = '';
selectedFile.value = null;
} else {
console.error(parsedResult);
showMessage(`上传失败: ${parsedResult.error}`, 'error');
}
} catch (parseError) {
console.error('解析Python返回结果失败:', parseError);
console.error('原始返回结果:', result);
showMessage(`上传失败: 返回结果解析错误`, 'error');
}
uploading.value = false;
}
);
};
reader.onerror = function() {
showMessage('文件读取失败', 'error');
uploading.value = false;
};
reader.readAsDataURL(selectedFile.value);
} else {
// 模拟上传成功
showMessage('插件上传成功', 'success');
pluginForm.value = { name: '', description: '' };
// 重置文件输入
document.getElementById('pluginFile').value = '';
selectedFile.value = null;
uploading.value = false;
}
} catch (error) {
showMessage(`上传失败: ${error.message}`, 'error');
uploading.value = false;
}
};
const showMessage = (text, type) => {
uploadMessage.value = { text, type };
setTimeout(() => {
uploadMessage.value = null;
}, 3000);
};
const showUploadForm = () => {
currentView.value = 'upload';
};
const listPlugins = () => {
currentView.value = 'list';
loadPlugins();
};
const showSettings = () => {
currentView.value = 'settings';
};
const loadPlugins = async () => {
loading.value = true;
try {
if (py && py.list_plugins) {
py.list_plugins((result) => {
try {
// 检查result是否为空或无效
if (!result) {
showMessage('加载插件列表失败: 服务器返回空结果', 'error');
loading.value = false;
return;
}
// 解析从Python返回的JSON字符串
const parsedResult = JSON.parse(result);
if (parsedResult.success) {
plugins.value = parsedResult.plugins;
} else {
showMessage(`加载插件列表失败: ${parsedResult.error}`, 'error');
}
} catch (parseError) {
console.error('解析Python返回结果失败:', parseError);
console.error('原始返回结果:', result);
showMessage(`加载插件列表失败: 返回结果解析错误`, 'error');
}
loading.value = false;
});
} else {
// 模拟插件数据
plugins.value = [
{ id: 1, name: '示例插件1', description: '这是一个示例插件', version: '1.0.0', uploadDate: '2023-01-01' },
{ id: 2, name: '示例插件2', description: '另一个示例插件', version: '2.1.0', uploadDate: '2023-02-15' }
];
loading.value = false;
}
} catch (error) {
showMessage(`加载插件列表失败: ${error.message}`, 'error');
loading.value = false;
}
};
const launchPlugin = (plugin) => {
if (py && py.launch_plugin) {
py.launch_plugin(plugin.id, (result) => {
try {
// 检查result是否为空或无效
if (!result) {
showMessage('启动插件失败: 服务器返回空结果', 'error');
return;
}
// 解析从Python返回的JSON字符串
const parsedResult = JSON.parse(result);
if (parsedResult.success) {
console.log(parsedResult.url)
} else {
showMessage(`启动插件失败: ${parsedResult.error}`, 'error');
}
} catch (parseError) {
console.error('解析Python返回结果失败:', parseError);
console.error('原始返回结果:', result);
showMessage(`启动插件失败: 返回结果解析错误`, 'error');
}
});
} else {
// 模拟启动插件
console.log(`启动插件: ${plugin.name}`);
}
};
const saveSettings = () => {
if (py && py.save_settings) {
// 将settings对象转换为JSON字符串再传递给Python
const settingsJson = JSON.stringify(settings.value);
py.save_settings(settingsJson, (result) => {
try {
// 检查result是否为空或无效
if (!result) {
showMessage('保存设置失败: 服务器返回空结果', 'error');
return;
}
// 解析从Python返回的JSON字符串
const parsedResult = JSON.parse(result);
if (parsedResult.success) {
showMessage('设置已保存', 'success');
} else {
showMessage(`保存设置失败: ${parsedResult.error}`, 'error');
}
} catch (parseError) {
console.error('解析Python返回结果失败:', parseError);
console.error('原始返回结果:', result);
showMessage(`保存设置失败: 返回结果解析错误`, 'error');
}
});
} else {
showMessage('设置已保存', 'success');
}
};
return {
currentView,
pluginForm,
uploading,
uploadMessage,
plugins,
loading,
settings,
handleFileUpload,
uploadPlugin,
showUploadForm,
listPlugins,
showSettings,
launchPlugin,
saveSettings
};
}
};
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 20px;
padding: 20px;
}
/* 自定义滚动条样式 */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
::-webkit-scrollbar-corner {
background: #f1f1f1;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.navbar {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.navbar button {
padding: 10px 20px;
background-color: #f0f0f0;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.navbar button:hover {
background-color: #e0e0e0;
}
.navbar button.active {
background-color: #42b983;
color: white;
}
.content {
min-height: 400px;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
button {
padding: 12px 24px;
background-color: #42b983;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover:not(:disabled) {
background-color: #359c6d;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.message {
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading, .no-plugins {
text-align: center;
padding: 40px;
color: #666;
}
.plugins-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.plugin-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.3s;
text-align: left;
}
.plugin-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.plugin-card h3 {
margin-top: 0;
color: #333;
}
.plugin-card p {
color: #666;
margin-bottom: 15px;
}
.plugin-meta {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #999;
}
.plugin-window {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
z-index: 1000;
}
.plugin-window-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #42b983;
color: white;
padding: 10px 20px;
}
.plugin-window-header button {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
}
.plugin-window-content {
height: calc(100% - 50px);
background-color: white;
}
@media (max-width: 768px) {
.plugins-grid {
grid-template-columns: 1fr;
}
.navbar {
flex-direction: column;
align-items: center;
}
.navbar button {
width: 100%;
margin-bottom: 10px;
}
}
</style>

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

-166
View File
@@ -1,166 +0,0 @@
<template>
<div id="app">
<div class="container">
<div id="sysTime" class="time-display">{{ sysTime }}</div>
<div class="message-form">
<input v-model="message" id="message" name="message" placeholder="输入消息" />
<button @click="sendMessage">发送消息</button>
</div>
<CustomAlert
v-if="alertVisible"
:title="alertTitle"
:message="alertMessage"
@close="alertVisible = false"
/>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { QWebChannel } from 'qwebchannel';
import CustomAlert from './components/CustomAlert.vue'
// ... existing code ...
export default {
name: 'App',
components: {
CustomAlert
},
mounted() {
window.app_sendMessage = async (data) => {
await this.sendMessage(data);
};
window.update_time = (timeStr) => {
this.update_time(timeStr);
}
},
setup() {
const message = ref('')
const sysTime = ref('')
const alertVisible = ref(false)
const alertTitle = ref('')
const alertMessage = ref('')
let py = null
// 初始化QWebChannel
onMounted(() => {
if (window.qt && window.qt.webChannelTransport) {
new QWebChannel(window.qt.webChannelTransport, function (channel) {
py = channel.objects.py;
console.log("update_time function registered in global scope");
// 延迟一小段时间后再通知Python端,确保channel完全建立
setTimeout(() => {
// 通知Python端前端已经准备就绪
if (channel.objects.py && channel.objects.py.on_frontend_ready) {
console.log("Calling on_frontend_ready");
channel.objects.py.on_frontend_ready();
} else {
console.log("on_frontend_ready not found");
}
}, 100);
});
} else {
// 模拟时间更新(当没有Python连接时)
setInterval(() => {
const currentTime = new Date().toLocaleString('zh-CN');
sysTime.value = `当前时间: ${currentTime}`;
}, 1000);
}
});
const hello = () => {
return new Promise((resolve) => {
if (py && py.hello) {
py.hello(message.value, (res) => {
resolve(res);
});
} else {
resolve(`hello from python: ${message.value}`);
}
});
};
const sendMessage = async (data = null) => {
try {
if (typeof data === "string"){
console.log("Sending message to Python:", data);
showAlert("信息", data);
return;
}
const result = await hello();
console.log(result);
showAlert("信息", result);
} catch (error) {
showAlert("错误", "发送消息时发送错误")
}
};
const showAlert = (title, message) => {
alertTitle.value = title;
alertMessage.value = message;
alertVisible.value = true;
};
const update_time = function(timeStr) {
sysTime.value = `python 本地时间: ${timeStr}`;
};
return {
message,
sysTime,
alertVisible,
alertTitle,
alertMessage,
sendMessage,
showAlert,
update_time
};
}
};
// ... existing code ...
</script>
<style>
.container {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.time-display {
padding: 5px;
border: 2px solid green;
margin-bottom: 10px;
min-height: 20px;
}
.message-form {
margin-top: 10px;
padding: 5px;
border: 2px solid green;
}
.message-form input {
padding: 8px;
margin-right: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.message-form button {
padding: 8px 16px;
background-color: #0078d4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.message-form button:hover {
background-color: #005a9e;
}
</style>
@@ -1,103 +0,0 @@
<template>
<div class="modal-overlay" @click="closeAlert">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ title }}</h3>
</div>
<div class="modal-body">
<p>{{ message }}</p>
</div>
<div class="modal-footer">
<button @click="closeAlert">确定</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CustomAlert',
props: {
title: {
type: String,
required: true
},
message: {
type: String,
required: true
}
},
emits: ['close'],
setup(props, { emit }) {
const closeAlert = () => {
emit('close');
};
return {
closeAlert
};
}
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
min-width: 300px;
max-width: 500px;
}
.modal-header {
padding: 15px;
background-color: #f0f0f0;
border-bottom: 1px solid #ddd;
border-radius: 5px 5px 0 0;
}
.modal-header h3 {
margin: 0;
font-weight: bold;
}
.modal-body {
padding: 20px;
}
.modal-body p {
margin: 0;
}
.modal-footer {
padding: 15px;
text-align: right;
border-top: 1px solid #eee;
}
.modal-footer button {
padding: 8px 16px;
background-color: #0078d4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.modal-footer button:hover {
background-color: #005a9e;
}
</style>