modify: h5 插件系统更新

This commit is contained in:
2025-09-13 12:13:52 +08:00
parent 3631b383d6
commit f94c4b57e4
43 changed files with 16649 additions and 1151 deletions
+2
View File
@@ -15,6 +15,7 @@ node_modules/
# python-build files # python-build files
output/ output/
output.md
# lib # lib
lib/ lib/
@@ -27,3 +28,4 @@ nuitka-crash-report.xml
# plugin files # plugin files
plugins/ plugins/
+6 -1
View File
@@ -1,3 +1,8 @@
import toml import toml
config = toml.load("pyproject.toml") try:
config = toml.load("pyproject.toml")
except:
config = {}
__all__ = ["config"]
@@ -9,3 +9,9 @@ def add_student(message_item: MessageItem):
# time.sleep(3) # time.sleep(3)
print("点击了添加按钮") print("点击了添加按钮")
return {"message": "学生添加成功"} return {"message": "学生添加成功"}
@TaskMounter.task(meta={"task_name": "test"})
def test(message_item: MessageItem):
# print(f"测试任务: {message_item}")
return {"message": "测试成功"}
@@ -6,4 +6,4 @@ import json
@ListenMounter.listener() @ListenMounter.listener()
def students(str_students_list: str): def students(str_students_list: str):
print("students signal received") print("students signal received")
listen_signals.listening_students_signal.emit(json.loads(str_students_list)) listen_signals.listening_students_signal.emit(json.loads(str_students_list))
+1 -1
View File
@@ -4,4 +4,4 @@ from .listen_signals import ListenSignals
global_signals = GlobalSignals() global_signals = GlobalSignals()
listen_signals = ListenSignals() listen_signals = ListenSignals()
__all__ = ["global_signals"] __all__ = ["global_signals"]
+3 -1
View File
@@ -4,5 +4,7 @@ from PySide6.QtCore import Signal, QObject
class GlobalSignals(QObject): class GlobalSignals(QObject):
show_add_dialog_signal = Signal(dict) show_add_dialog_signal = Signal(dict)
update_queue_state = Signal(dict, str)
__all__ = ["GlobalSignals"]
__all__ = ["GlobalSignals"]
+2 -1
View File
@@ -5,4 +5,5 @@ class ListenSignals(QObject):
listening_students_signal = Signal(list) listening_students_signal = Signal(list)
__all__ = ["ListenSignals"]
__all__ = ["ListenSignals"]
+24 -6
View File
@@ -15,7 +15,9 @@ QPushButton:pressed{
} }
""" """
ADD_BUTTON_STYLE = BUTTON_STYLE + """ ADD_BUTTON_STYLE = (
BUTTON_STYLE
+ """
QPushButton { QPushButton {
background-color: #0d6efd; background-color: #0d6efd;
} }
@@ -26,8 +28,11 @@ QPushButton:pressed{
background-color: #0a58ca; background-color: #0a58ca;
} }
""" """
)
DELETE_BUTTON_STYLE = BUTTON_STYLE + """ DELETE_BUTTON_STYLE = (
BUTTON_STYLE
+ """
QPushButton { QPushButton {
background-color: #dc3545; background-color: #dc3545;
} }
@@ -37,8 +42,11 @@ QPushButton:hover {
QPushButton:pressed{ QPushButton:pressed{
background-color: #b02a37; background-color: #b02a37;
""" """
)
BATCH_DELETE_BUTTON_STYLE = BUTTON_STYLE + """ BATCH_DELETE_BUTTON_STYLE = (
BUTTON_STYLE
+ """
QPushButton { QPushButton {
background-color: #fd7e14; background-color: #fd7e14;
} }
@@ -49,8 +57,11 @@ QPushButton:pressed{
background-color: #dc680f; background-color: #dc680f;
} }
""" """
)
UPDATE_BUTTON_STYLE = BUTTON_STYLE + """ UPDATE_BUTTON_STYLE = (
BUTTON_STYLE
+ """
QPushButton { QPushButton {
background-color: #198754; background-color: #198754;
} }
@@ -61,8 +72,11 @@ QPushButton:pressed{
background-color: #146c43; background-color: #146c43;
} }
""" """
)
IMPORT_BUTTON_STYLE = BUTTON_STYLE + """ IMPORT_BUTTON_STYLE = (
BUTTON_STYLE
+ """
QPushButton { QPushButton {
background-color: #6f42c1; background-color: #6f42c1;
} }
@@ -73,8 +87,11 @@ QPushButton:pressed{
background-color: #4a2d8e; background-color: #4a2d8e;
} }
""" """
)
EXPORT_BUTTON_STYLE = BUTTON_STYLE + """ EXPORT_BUTTON_STYLE = (
BUTTON_STYLE
+ """
QPushButton { QPushButton {
background-color: #20c997; background-color: #20c997;
} }
@@ -85,3 +102,4 @@ QPushButton:pressed{
background-color: #198b6d; background-color: #198b6d;
} }
""" """
)
+34 -13
View File
@@ -1,14 +1,30 @@
# coding:utf-8 # coding:utf-8
import os
import sys
from PySide6.QtCore import QUrl, QSize, QEventLoop, QTimer from PySide6.QtCore import QUrl, QSize, QEventLoop, QTimer
from PySide6.QtGui import QIcon, QDesktopServices from PySide6.QtGui import QIcon, QDesktopServices
from qfluentwidgets import (NavigationItemPosition, MessageBox, NavigationAvatarWidget, SplitFluentWindow, SplashScreen) from PySide6.QtWidgets import QApplication
from qfluentwidgets import (
NavigationItemPosition,
MessageBox,
NavigationAvatarWidget,
SplitFluentWindow,
SplashScreen,
)
from qfluentwidgets import FluentIcon as FIF from qfluentwidgets import FluentIcon as FIF
from .students.student_interface import StudentInterface from .students.student_interface import StudentInterface
from .video.video_interface import VideoInterface from .video.video_interface import VideoInterface
from .camera.camera_interface import CameraInterface from .camera.camera_interface import CameraInterface
from .web_view.web_view_interface import WebViewInterface from .web_view.web_view_interface import WebViewInterface
from PySide6.QtWidgets import QApplication
def resource_path(relative_path):
"""获取资源的绝对路径"""
if hasattr(sys, "_MEIPASS"):
return os.path.join(sys._MEIPASS, relative_path) # type: ignore
return os.path.join(os.path.abspath("."), relative_path)
class Window(SplitFluentWindow): class Window(SplitFluentWindow):
@@ -21,18 +37,20 @@ class Window(SplitFluentWindow):
self.web_view_interface = WebViewInterface(self) self.web_view_interface = WebViewInterface(self)
self.initNavigation() self.initNavigation()
self.resize(900, 700) self.resize(900, 700)
self.setWindowIcon(QIcon('./resources/images/logo.png'))
# 使用资源路径函数
self.setWindowIcon(QIcon(resource_path("./resources/images/logo.png")))
desktop = QApplication.screens()[0].availableGeometry() desktop = QApplication.screens()[0].availableGeometry()
w, h = desktop.width(), desktop.height() w, h = desktop.width(), desktop.height()
self.move(w//2 - self.width()//2, h//2 - self.height()//2) self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
self.splashScreen = SplashScreen(self.windowIcon(), self) self.splashScreen = SplashScreen(self.windowIcon(), self)
self.splashScreen.setIconSize(QSize(102, 102)) self.splashScreen.setIconSize(QSize(102, 102))
self.show() self.show()
self.createSubInterface() self.createSubInterface()
self.splashScreen.finish() self.splashScreen.finish()
def createSubInterface(self): def createSubInterface(self):
loop = QEventLoop(self) loop = QEventLoop(self)
QTimer.singleShot(2000, loop.quit) QTimer.singleShot(2000, loop.quit)
@@ -44,17 +62,20 @@ class Window(SplitFluentWindow):
self.addSubInterface(self.camera_interface, FIF.CAMERA, "Camera") self.addSubInterface(self.camera_interface, FIF.CAMERA, "Camera")
self.addSubInterface(self.web_view_interface, FIF.FIT_PAGE, "Web View") self.addSubInterface(self.web_view_interface, FIF.FIT_PAGE, "Web View")
# 使用资源路径函数
self.navigationInterface.addWidget( self.navigationInterface.addWidget(
routeKey="Info", routeKey="Info",
widget=NavigationAvatarWidget('Info', 'resource/images/logo.png'), widget=NavigationAvatarWidget(
"Info", resource_path("./resource/images/logo.png")
),
onClick=self.showMessageBox, onClick=self.showMessageBox,
position=NavigationItemPosition.BOTTOM, position=NavigationItemPosition.BOTTOM,
) )
self.navigationInterface.addItem( self.navigationInterface.addItem(
routeKey='settingInterface', routeKey="settingInterface",
icon=FIF.SETTING, icon=FIF.SETTING,
text='设置', text="设置",
position=NavigationItemPosition.BOTTOM, position=NavigationItemPosition.BOTTOM,
) )
@@ -62,12 +83,12 @@ class Window(SplitFluentWindow):
def showMessageBox(self): def showMessageBox(self):
w = MessageBox( w = MessageBox(
'支持作者🥰', "支持作者🥰",
'个人开发不易,如果这个项目帮助到了您,可以考虑请作者喝一瓶快乐水🥤。您的支持就是作者开发和维护项目的动力🚀', "个人开发不易,如果这个项目帮助到了您,可以考虑请作者喝一瓶快乐水🥤。您的支持就是作者开发和维护项目的动力🚀",
self self,
) )
w.yesButton.setText('来啦老弟') w.yesButton.setText("来啦老弟")
w.cancelButton.setText('下次一定') w.cancelButton.setText("下次一定")
if w.exec(): if w.exec():
QDesktopServices.openUrl(QUrl("https://afdian.net/a/zhiyiYo")) QDesktopServices.openUrl(QUrl("https://afdian.net/a/zhiyiYo"))
+28 -25
View File
@@ -18,25 +18,25 @@ class VideoStream(QObject):
self.timer.timeout.connect(self.update_frame) self.timer.timeout.connect(self.update_frame)
self.retry_count = 0 self.retry_count = 0
self.max_retry = 5 self.max_retry = 5
self.open_stream() self.open_stream()
def open_stream(self): def open_stream(self):
"""尝试打开RTSP视频流""" """尝试打开RTSP视频流"""
if self.cap: if self.cap:
self.cap.release() self.cap.release()
# 创建VideoCapture对象并设置RTSP参数 # 创建VideoCapture对象并设置RTSP参数
self.cap = cv2.VideoCapture(self.stream_url) self.cap = cv2.VideoCapture(self.stream_url)
# 关键设置:使用TCP传输(避免UDP丢包问题),设置超时和缓冲区 # 关键设置:使用TCP传输(避免UDP丢包问题),设置超时和缓冲区
# self.cap.set(cv2.CAP_PROP_RTSP_TRANSPORT, 1) # 1 = TCP传输模式 # self.cap.set(cv2.CAP_PROP_RTSP_TRANSPORT, 1) # 1 = TCP传输模式
self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) # 5秒超时 self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) # 5秒超时
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 最小化缓冲区 self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 最小化缓冲区
# 打开RTSP流 # 打开RTSP流
self.cap.open(self.stream_url) self.cap.open(self.stream_url)
if not self.cap.isOpened(): if not self.cap.isOpened():
self.retry_count += 1 self.retry_count += 1
if self.retry_count <= self.max_retry: if self.retry_count <= self.max_retry:
@@ -46,11 +46,11 @@ class VideoStream(QObject):
else: else:
self.retry_count = 0 self.retry_count = 0
self.timer.start(30) # 约30fps self.timer.start(30) # 约30fps
def update_frame(self): def update_frame(self):
if not self.cap or not self.cap.isOpened(): if not self.cap or not self.cap.isOpened():
return return
ret, frame = self.cap.read() ret, frame = self.cap.read()
if ret: if ret:
# 转换为黑白图像 # 转换为黑白图像
@@ -67,24 +67,25 @@ class VideoStream(QObject):
else: else:
# 读取失败时重新连接 # 读取失败时重新连接
self.open_stream() self.open_stream()
def stop(self): def stop(self):
self.timer.stop() self.timer.stop()
if self.cap: if self.cap:
self.cap.release() self.cap.release()
class CameraInterface(QWidget): class CameraInterface(QWidget):
def __init__(self, rtsp_url="rtsp://127.0.0.1:8554/stream"): def __init__(self, rtsp_url="rtsp://127.0.0.1:8554/stream"):
super().__init__() super().__init__()
self.setObjectName("camera_interface") self.setObjectName("camera_interface")
self.setWindowTitle("RTSP视频流接收器") self.setWindowTitle("RTSP视频流接收器")
self.resize(800, 600) self.resize(800, 600)
# 创建UI # 创建UI
self.label = QLabel("等待RTSP视频流...") self.label = QLabel("等待RTSP视频流...")
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.label.setStyleSheet("font-size: 20px; color: #888;") self.label.setStyleSheet("font-size: 20px; color: #888;")
# 创建按钮 # 创建按钮
self.listen_button = PushButton("开始监听RTSP") self.listen_button = PushButton("开始监听RTSP")
self.listen_button.clicked.connect(lambda: self.start_listen(rtsp_url)) self.listen_button.clicked.connect(lambda: self.start_listen(rtsp_url))
@@ -94,7 +95,7 @@ class CameraInterface(QWidget):
layout.addWidget(self.label) layout.addWidget(self.label)
layout.addWidget(self.listen_button) layout.addWidget(self.listen_button)
self.setLayout(layout) self.setLayout(layout)
def start_listen(self, rtsp_url): def start_listen(self, rtsp_url):
self.listen_button.setEnabled(False) self.listen_button.setEnabled(False)
self.label.setText(f"正在连接: {rtsp_url}...") self.label.setText(f"正在连接: {rtsp_url}...")
@@ -102,35 +103,37 @@ class CameraInterface(QWidget):
self.stream = VideoStream(rtsp_url) self.stream = VideoStream(rtsp_url)
self.stream.new_frame.connect(self.display_frame) self.stream.new_frame.connect(self.display_frame)
self.stream.error_occurred.connect(self.display_error) self.stream.error_occurred.connect(self.display_error)
def display_frame(self, image): def display_frame(self, image):
pixmap = QPixmap.fromImage(image) pixmap = QPixmap.fromImage(image)
self.label.setPixmap(pixmap.scaled( self.label.setPixmap(
self.label.width(), pixmap.scaled(
self.label.height(), self.label.width(),
Qt.AspectRatioMode.KeepAspectRatio, self.label.height(),
Qt.TransformationMode.SmoothTransformation Qt.AspectRatioMode.KeepAspectRatio,
)) Qt.TransformationMode.SmoothTransformation,
)
)
def display_error(self, message): def display_error(self, message):
QMessageBox.critical(self, "RTSP视频流错误", message) QMessageBox.critical(self, "RTSP视频流错误", message)
self.label.setText(f"错误: {message}") self.label.setText(f"错误: {message}")
self.listen_button.setEnabled(True) # 出错时重新启用按钮 self.listen_button.setEnabled(True) # 出错时重新启用按钮
def closeEvent(self, event): def closeEvent(self, event):
if hasattr(self, 'stream') and self.stream: if hasattr(self, "stream") and self.stream:
self.stream.stop() self.stream.stop()
super().closeEvent(event) super().closeEvent(event)
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
# 使用命令行参数指定RTSP URL # 使用命令行参数指定RTSP URL
rtsp_url = "rtsp://127.0.0.1:8554/stream" # 默认URL rtsp_url = "rtsp://127.0.0.1:8554/stream" # 默认URL
if len(sys.argv) > 1: if len(sys.argv) > 1:
rtsp_url = sys.argv[1] rtsp_url = sys.argv[1]
player = CameraInterface(rtsp_url) player = CameraInterface(rtsp_url)
player.show() player.show()
sys.exit(app.exec()) sys.exit(app.exec())
+4 -4
View File
@@ -14,7 +14,7 @@ class BaseStudentDilalog(MessageBoxBase):
self.titleLabel = SubtitleLabel(self.title, self) self.titleLabel = SubtitleLabel(self.title, self)
self.viewLayout.addWidget(self.titleLabel) self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.setAlignment(self.titleLabel, Qt.AlignmentFlag.AlignCenter) self.viewLayout.setAlignment(self.titleLabel, Qt.AlignmentFlag.AlignCenter)
grid_layout = QGridLayout() grid_layout = QGridLayout()
self.viewLayout.addLayout(grid_layout) self.viewLayout.addLayout(grid_layout)
@@ -48,8 +48,8 @@ class BaseStudentDilalog(MessageBoxBase):
grid_layout.setColumnStretch(1, 1) grid_layout.setColumnStretch(1, 1)
grid_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) grid_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.yesButton.setText('确定') self.yesButton.setText("确定")
self.cancelButton.setText('取消') self.cancelButton.setText("取消")
for widget in [field[1] for field in fields]: for widget in [field[1] for field in fields]:
widget.setMinimumWidth(200) widget.setMinimumWidth(200)
@@ -64,4 +64,4 @@ class AddStudentDilalog(BaseStudentDilalog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__("添加学生", parent) super().__init__("添加学生", parent)
self.student_id = None self.student_id = None
self.yesButton.setText('添加') self.yesButton.setText("添加")
+109 -18
View File
@@ -1,12 +1,27 @@
import json import json
import os
import sys
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtCore import Slot from PySide6.QtCore import Slot
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QHeaderView, QCheckBox, QTableWidgetItem from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QHeaderView,
QCheckBox,
QTableWidgetItem,
)
from queue_sqlite.model import MessageItem from queue_sqlite.model import MessageItem
from qfluentwidgets import CardWidget, PushButton, SearchLineEdit, TableWidget, setCustomStyleSheet from qfluentwidgets import (
CardWidget,
PushButton,
SearchLineEdit,
TableWidget,
setCustomStyleSheet,
)
from .student_dilalog import AddStudentDilalog from .student_dilalog import AddStudentDilalog
from ...style.button_style import ADD_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE from ...style.button_style import ADD_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE
@@ -15,13 +30,20 @@ from ...signal import listen_signals
from ...scheduler_manager import scheduler from ...scheduler_manager import scheduler
def resource_path(relative_path):
"""获取资源的绝对路径"""
if hasattr(sys, "_MEIPASS"):
return os.path.join(sys._MEIPASS, relative_path) # type: ignore
return os.path.join(os.path.abspath("."), relative_path)
class StudentInterface(QWidget): class StudentInterface(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setObjectName("student_interface") self.setObjectName("student_interface")
self.students = [] self.students = []
self.setup_ui() self.setup_ui()
self.create_signal_slot() self.create_signal_slot()
self.load_data() self.load_data()
self.populate_table(self.students) self.populate_table(self.students)
@@ -34,19 +56,21 @@ class StudentInterface(QWidget):
# 设置 title # 设置 title
self.setWindowTitle("学生管理") self.setWindowTitle("学生管理")
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setContentsMargins(20, 50, 20, 20) # (左, 上, 右, 下) layout.setContentsMargins(20, 50, 20, 20) # (左, 上, 右, 下)
card_widget = CardWidget(self) card_widget = CardWidget(self)
buttons_layout = QHBoxLayout(card_widget) buttons_layout = QHBoxLayout(card_widget)
self.addButton = PushButton("新增", self) self.addButton = PushButton("新增", self)
setCustomStyleSheet(self.addButton, ADD_BUTTON_STYLE, ADD_BUTTON_STYLE) setCustomStyleSheet(self.addButton, ADD_BUTTON_STYLE, ADD_BUTTON_STYLE)
self.addButton.clicked.connect(self.add_student) self.addButton.clicked.connect(self.add_student)
self.searchInput = SearchLineEdit(self) self.searchInput = SearchLineEdit(self)
self.searchInput.setPlaceholderText("搜索学生姓名或学号...") self.searchInput.setPlaceholderText("搜索学生姓名或学号...")
self.searchInput.setFixedWidth(500) self.searchInput.setFixedWidth(500)
self.batchDeleteButton = PushButton("批量删除", self) self.batchDeleteButton = PushButton("批量删除", self)
setCustomStyleSheet(self.batchDeleteButton, BATCH_DELETE_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE) setCustomStyleSheet(
self.batchDeleteButton, BATCH_DELETE_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE
)
buttons_layout.addWidget(self.addButton) buttons_layout.addWidget(self.addButton)
buttons_layout.addWidget(self.searchInput) buttons_layout.addWidget(self.searchInput)
@@ -56,25 +80,89 @@ class StudentInterface(QWidget):
layout.addWidget(card_widget) layout.addWidget(card_widget)
self.table_widget = TableWidget(self) self.table_widget = TableWidget(self)
self.table_widget.setBorderRadius(8) # 设置边框圆角 self.table_widget.setBorderRadius(8) # 设置边框圆角
self.table_widget.setBorderVisible(True) # 设置表格边框可见 self.table_widget.setBorderVisible(True) # 设置表格边框可见
table_labels = ["", "学生ID", "姓名", "学号", "性别", "班级", "语文", "数学", "英语", "总分", "操作"] table_labels = [
"",
"学生ID",
"姓名",
"学号",
"性别",
"班级",
"语文",
"数学",
"英语",
"总分",
"操作",
]
self.table_widget.setColumnCount(len(table_labels)) self.table_widget.setColumnCount(len(table_labels))
self.table_widget.setHorizontalHeaderLabels(table_labels) self.table_widget.setHorizontalHeaderLabels(table_labels)
self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.table_widget.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch
)
layout.addWidget(self.table_widget) layout.addWidget(self.table_widget)
self.setStyleSheet("StudentInterface {background: white}") self.setStyleSheet("StudentInterface {background: white}")
self.setWindowIcon(QIcon('./resources/images/logo.png')) self.setWindowIcon(QIcon(resource_path("./resources/images/logo.png")))
def load_data(self): def load_data(self):
self.students = [ self.students = [
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265}, {
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265}, "student_id": 1,
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265}, "student_name": "张三",
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265}, "student_number": "2024010101",
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265}, "gender": 1,
"class_name": "1班",
"chinese_score": 80,
"math_score": 85,
"english_score": 90,
"total_score": 265,
},
{
"student_id": 1,
"student_name": "张三",
"student_number": "2024010101",
"gender": 1,
"class_name": "1班",
"chinese_score": 80,
"math_score": 85,
"english_score": 90,
"total_score": 265,
},
{
"student_id": 1,
"student_name": "张三",
"student_number": "2024010101",
"gender": 1,
"class_name": "1班",
"chinese_score": 80,
"math_score": 85,
"english_score": 90,
"total_score": 265,
},
{
"student_id": 1,
"student_name": "张三",
"student_number": "2024010101",
"gender": 1,
"class_name": "1班",
"chinese_score": 80,
"math_score": 85,
"english_score": 90,
"total_score": 265,
},
{
"student_id": 1,
"student_name": "张三",
"student_number": "2024010101",
"gender": 1,
"class_name": "1班",
"chinese_score": 80,
"math_score": 85,
"english_score": 90,
"total_score": 265,
},
] ]
scheduler.update_listen_data("students", json.dumps(self.students)) scheduler.update_listen_data("students", json.dumps(self.students))
@@ -94,7 +182,7 @@ class StudentInterface(QWidget):
if key == "gender": if key == "gender":
value = "男" if value == 1 else "女" if value == 2 else "未知" value = "男" if value == 1 else "女" if value == 2 else "未知"
item = QTableWidgetItem(str(value)) item = QTableWidgetItem(str(value))
self.table_widget.setItem(row, col+1, item) self.table_widget.setItem(row, col + 1, item)
@Slot(dict) @Slot(dict)
def show_add_student_dialog(self, message_dict: dict): def show_add_student_dialog(self, message_dict: dict):
@@ -113,4 +201,7 @@ class StudentInterface(QWidget):
def add_student(self): def add_student(self):
self.addButton.setEnabled(False) self.addButton.setEnabled(False)
scheduler.send_message(MessageItem(content={}, destination="add_student"), self.add_student_callback) scheduler.send_message(
MessageItem(content={}, destination="add_student"),
self.add_student_callback,
)
+25 -23
View File
@@ -9,19 +9,20 @@ class VideoInterface(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
from PySide6.QtMultimedia import QMediaPlayer from PySide6.QtMultimedia import QMediaPlayer
self.player = QMediaPlayer() self.player = QMediaPlayer()
self.setObjectName("video_interface") self.setObjectName("video_interface")
self.setWindowTitle("Fluent Video Player") self.setWindowTitle("Fluent Video Player")
self.resize(900, 600) self.resize(900, 600)
# 设置主题 # 设置主题
# setTheme(Theme.DARK) # setTheme(Theme.DARK)
# 创建主布局 # 创建主布局
self.main_layout = QVBoxLayout(self) self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(20, 50, 20, 20) self.main_layout.setContentsMargins(20, 50, 20, 20)
self.main_layout.setSpacing(15) self.main_layout.setSpacing(15)
# 创建标题标签 - 修复字体设置 # 创建标题标签 - 修复字体设置
self.title_label = QLabel("Fluent Video Player") self.title_label = QLabel("Fluent Video Player")
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -30,27 +31,26 @@ class VideoInterface(QWidget):
title_font.setPointSize(24) title_font.setPointSize(24)
title_font.setBold(True) # 使用setBold方法设置粗体 title_font.setBold(True) # 使用setBold方法设置粗体
self.title_label.setFont(title_font) self.title_label.setFont(title_font)
self.main_layout.addWidget(self.title_label) self.main_layout.addWidget(self.title_label)
# 创建视频播放器 # 创建视频播放器
self.video_widget = VideoWidget() self.video_widget = VideoWidget()
self.video_widget.setMinimumHeight(400) self.video_widget.setMinimumHeight(400)
self.main_layout.addWidget(self.video_widget, 1) self.main_layout.addWidget(self.video_widget, 1)
# 创建控制面板 # 创建控制面板
self.control_layout = QHBoxLayout() self.control_layout = QHBoxLayout()
self.control_layout.setContentsMargins(10, 10, 10, 10) self.control_layout.setContentsMargins(10, 10, 10, 10)
self.control_layout.setSpacing(15) self.control_layout.setSpacing(15)
self.main_layout.addLayout(self.control_layout) self.main_layout.addLayout(self.control_layout)
# 添加控制按钮 # 添加控制按钮
self.open_button = PushButton(FluentIcon.FOLDER_ADD, "打开视频") self.open_button = PushButton(FluentIcon.FOLDER_ADD, "打开视频")
self.open_button.setFixedWidth(120) self.open_button.setFixedWidth(120)
self.open_button.clicked.connect(self.open_video) self.open_button.clicked.connect(self.open_video)
self.control_layout.addWidget(self.open_button) self.control_layout.addWidget(self.open_button)
# 添加状态标签 - 修复字体设置 # 添加状态标签 - 修复字体设置
self.status_label = QLabel("准备就绪") self.status_label = QLabel("准备就绪")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -58,11 +58,11 @@ class VideoInterface(QWidget):
status_font.setPointSize(12) status_font.setPointSize(12)
self.status_label.setFont(status_font) self.status_label.setFont(status_font)
self.control_layout.addWidget(self.status_label) self.control_layout.addWidget(self.status_label)
# 设置初始状态 # 设置初始状态
self.video_loaded = False self.video_loaded = False
self.is_fullscreen = False self.is_fullscreen = False
# 设置样式 # 设置样式
# self.setStyleSheet(""" # self.setStyleSheet("""
# QWidget { # QWidget {
@@ -73,38 +73,40 @@ class VideoInterface(QWidget):
# padding: 5px; # padding: 5px;
# } # }
# """) # """)
def open_video(self): def open_video(self):
"""打开视频文件""" """打开视频文件"""
file_path, _ = QFileDialog.getOpenFileName( file_path, _ = QFileDialog.getOpenFileName(
self, "选择视频文件", "", self,
"视频文件 (*.mp4 *.avi *.mkv *.mov *.flv *.wmv);;所有文件 (*.*)" "选择视频文件",
"",
"视频文件 (*.mp4 *.avi *.mkv *.mov *.flv *.wmv);;所有文件 (*.*)",
) )
if file_path: if file_path:
# 设置视频URL # 设置视频URL
self.video_widget.setVideo(QUrl.fromLocalFile(file_path)) self.video_widget.setVideo(QUrl.fromLocalFile(file_path))
self.status_label.setText(f"已加载视频: {file_path.split('/')[-1]}") self.status_label.setText(f"已加载视频: {file_path.split('/')[-1]}")
self.video_loaded = True self.video_loaded = True
def play_video(self): def play_video(self):
"""播放视频""" """播放视频"""
if self.video_loaded: if self.video_loaded:
self.video_widget.play() self.video_widget.play()
self.status_label.setText("正在播放...") self.status_label.setText("正在播放...")
def pause_video(self): def pause_video(self):
"""暂停视频""" """暂停视频"""
if self.video_loaded: if self.video_loaded:
self.video_widget.pause() self.video_widget.pause()
self.status_label.setText("已暂停") self.status_label.setText("已暂停")
def stop_video(self): def stop_video(self):
"""停止视频""" """停止视频"""
if self.video_loaded: if self.video_loaded:
self.video_widget.stop() self.video_widget.stop()
self.status_label.setText("已停止") self.status_label.setText("已停止")
def toggle_fullscreen(self): def toggle_fullscreen(self):
"""切换全屏模式""" """切换全屏模式"""
if self.is_fullscreen: if self.is_fullscreen:
@@ -112,15 +114,15 @@ class VideoInterface(QWidget):
else: else:
self.showFullScreen() self.showFullScreen()
self.is_fullscreen = True self.is_fullscreen = True
# if __name__ == "__main__": # if __name__ == "__main__":
# app = QApplication(sys.argv) # app = QApplication(sys.argv)
# # 设置应用程序样式 # # 设置应用程序样式
# from qfluentwidgets import setThemeColor # from qfluentwidgets import setThemeColor
# setThemeColor('#28a9e0') # setThemeColor('#28a9e0')
# player = VideoPlayer() # player = VideoPlayer()
# player.show() # player.show()
# sys.exit(app.exec()) # sys.exit(app.exec())
+245 -53
View File
@@ -1,41 +1,100 @@
from PySide6.QtCore import QObject, Slot, QUrl
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtWebEngineCore import QWebEngineSettings
import time
import os import os
import time
import zipfile import zipfile
from pathlib import Path
import json import json
import shutil import shutil
import base64 import base64
import tempfile import tempfile
from pathlib import Path
from PySide6.QtCore import QObject, Slot, QUrl
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtWebEngineCore import QWebEngineSettings
from qframelesswindow.webengine import FramelessWebEngineView
from queue_sqlite.model import MessageItem
from ...scheduler_manager import scheduler
from ...signal import global_signals
class WebViewQueueSqliteOperations(QObject):
"""
用于前端调用QueueSqlite的队列操作
"""
def __init__(self, webview: FramelessWebEngineView, parent=None):
super().__init__(parent)
self.webview = webview
self.page = self.webview.page()
global_signals.update_queue_state.connect(self.update_queue_state)
@Slot(str, str, result=str)
def send_message(self, message: str, store_key: str):
def callback(result: MessageItem):
print(f"send_message callback: {result}")
global_signals.update_queue_state.emit(result.to_dict(), store_key)
scheduler.send_message(MessageItem.from_dict(json.loads(message)), callback)
def update_queue_state(self, result: dict, store_key: str):
json_str = json.dumps(result)
print(f"result: {result}")
print(f"json_str: {json_str}")
js_code = f"""
try {{
if (typeof window.queueStore.updateQueueData === 'function') {{
window.queueStore.updateQueueData('{store_key}', {json_str});
}} else {{
console.log('updateQueueData function not found');
}}
}} catch (error) {{
console.error('Error in updateQueueData:', error);
}}
"""
print(f"执行的JavaScript代码: {js_code}")
self.page.runJavaScript(js_code)
class WebviewBridge(QObject): class WebviewBridge(QObject):
"""Webview桥接类,用于处理前端和Python后端之间的通信""" """Webview桥接类,用于处理前端和Python后端之间的通信"""
def __init__(self, webview, parent=None): def __init__(self, webview, parent=None):
super().__init__(parent) super().__init__(parent)
self.webview = webview self.webview = webview
# 启用本地文件访问 # 启用本地文件访问
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True) self.webview.settings().setAttribute(
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True) )
self.webview.settings().setAttribute(QWebEngineSettings.WebAttribute.AllowGeolocationOnInsecureOrigins, 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安全策略 # 设置Web安全策略
self.webview.page().settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True) self.webview.page().settings().setAttribute(
self.webview.page().settings().setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True
self.webview.page().settings().setAttribute(QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, 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 = Path("plugins")
self.plugins_dir.mkdir(exist_ok=True) os.makedirs(self.plugins_dir, exist_ok=True)
# 插件信息文件 # 插件信息文件
self.plugins_info_file = self.plugins_dir / "plugins.json" self.plugins_info_file = self.plugins_dir / "plugins.json"
if not self.plugins_info_file.exists(): if not self.plugins_info_file.exists():
with open(self.plugins_info_file, 'w') as f: with open(self.plugins_info_file, "w") as f:
json.dump([], f) json.dump([], f)
@Slot(str, str, str, result=str) @Slot(str, str, str, result=str)
@@ -44,55 +103,181 @@ class WebviewBridge(QObject):
try: try:
# 将base64编码的文件内容解码并保存为临时文件 # 将base64编码的文件内容解码并保存为临时文件
file_data = base64.b64decode(file_content) file_data = base64.b64decode(file_content)
# 创建临时文件来保存上传的ZIP文件 # 创建临时文件来保存上传的ZIP文件
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_file: with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
tmp_file.write(file_data) tmp_file.write(file_data)
tmp_file_path = tmp_file.name tmp_file_path = tmp_file.name
# 判断插件是否已存在
if os.path.exists(self.plugins_dir / name):
os.unlink(tmp_file_path)
return json.dumps({"success": False, "error": "插件名称已存在"})
# 解压插件 # 解压插件
success, error = self.extract_plugin(tmp_file_path, name) success, msg = self.extract_plugin(tmp_file_path, name)
if not success: if not success:
# 清理临时文件 # 清理临时文件
os.unlink(tmp_file_path) os.unlink(tmp_file_path)
return json.dumps({"success": False, "error": error}) return json.dumps({"success": False, "error": msg})
# 读取 plugin.config.json 文件
plugin_config_file = msg.replace("index.html", "plugin.config.json")
if not os.path.exists(plugin_config_file):
os.unlink(tmp_file_path)
return json.dumps({"success": False, "error": "插件配置文件不存在"})
with open(plugin_config_file, "r") as f:
plugin_config = json.load(f)
plugin_conofig_keys = [
"pluginName",
"pluginId",
"pluginDescription",
"pluginVersion",
"pluginAuthor",
]
for key in plugin_conofig_keys:
if key not in plugin_config:
os.unlink(tmp_file_path)
return json.dumps(
{"success": False, "error": "插件配置文件缺少必要字段"}
)
plugin_info = { plugin_info = {
"id": int(time.time()), # 使用时间戳作为简单ID "id": plugin_config["pluginId"],
"name": name, "name": name,
"pluginName": plugin_config["pluginName"],
"description": description, "description": description,
"version": "1.0.0", "version": plugin_config["pluginVersion"],
"uploadDate": time.strftime('%Y-%m-%d', time.localtime()), "author": plugin_config["pluginAuthor"],
"path": str(self.plugins_dir / name) "uploadDate": time.strftime("%Y-%m-%d", time.localtime()),
"path": str(self.plugins_dir / name / plugin_config["pluginVersion"]),
} }
# 创建版本路径并移动插件文件
version_path = self.plugins_dir / name / plugin_info["version"]
version_path.mkdir(exist_ok=True)
for file in os.listdir(os.path.dirname(msg)):
shutil.move(os.path.join(os.path.dirname(msg), file), version_path)
# 读取现有插件信息 # 读取现有插件信息
if self.plugins_info_file.exists(): if os.path.exists(self.plugins_info_file):
with open(self.plugins_info_file, 'r') as f: with open(self.plugins_info_file, "r") as f:
plugins = json.load(f) plugins = json.load(f)
else: else:
plugins = [] plugins = []
# 判断pluginId是否存在
if plugin_info["id"] in [p["id"] for p in plugins]:
os.unlink(tmp_file_path)
return json.dumps({"success": False, "error": "插件ID已存在"})
# 添加新插件 # 添加新插件
plugins.append(plugin_info) plugins.append(plugin_info)
# 保存插件信息 # 保存插件信息
with open(self.plugins_info_file, 'w') as f: with open(self.plugins_info_file, "w") as f:
json.dump(plugins, f) json.dump(plugins, f)
# 清理临时文件 # 清理临时文件
os.unlink(tmp_file_path) os.unlink(tmp_file_path)
return json.dumps({"success": True}) return json.dumps({"success": True})
except Exception as e: except Exception as e:
# 确保任何异常都返回有效的JSON # 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)}) return json.dumps({"success": False, "error": str(e)})
@Slot(str, result=str)
def update_plugin(self, plugin_id, name, description, file_content):
"""处理插件更新请求"""
try:
file_data = base64.b64decode(file_content)
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
tmp_file.write(file_data)
tmp_filepath = tmp_file.name
success, msg = self.extract_plugin(tmp_filepath, name)
if not success:
os.unlink(tmp_filepath)
return json.dumps({"success": False, "error": msg})
plugin_config_file = msg.replace("index.html", "plugin.config.json")
if not os.path.exists(plugin_config_file):
os.unlink(tmp_filepath)
return json.dumps({"success": False, "error": "插件配置文件不存在"})
with open(plugin_config_file, "r") as f:
plugin_config = json.load(f)
plugin_config_keys = [
"pluginName",
"pluginId",
"pluginDescription",
"pluginVersion",
"pluginAuthor",
]
for key in plugin_config_keys:
if key not in plugin_config:
os.unlink(tmp_filepath)
return json.dumps(
{"success": False, "error": "插件配置文件缺少必要字段"}
)
# 判断插件是否存在
if not os.path.exists(self.plugins_dir / name):
os.unlink(tmp_filepath)
return json.dumps({"success": False, "error": "插件不存在"})
# 判断版本是否已存在
version_path = self.plugins_dir / name / plugin_config["pluginVersion"]
if version_path.exists():
os.unlink(tmp_filepath)
return json.dumps({"success": False, "error": "版本已存在"})
# 创建版本路径并移动插件文件
version_path.mkdir(exist_ok=True)
for file in os.listdir(os.path.dirname(msg)):
shutil.move(os.path.join(os.path.dirname(msg), file), version_path)
# 更新插件信息
with open(self.plugins_info_file, "r") as f:
plugins = json.load(f)
plugin_info = next((p for p in plugins if p["id"] == plugin_id), None)
if plugin_info:
plugin_info["name"] = name
plugin_info["description"] = description
plugin_info["version"] = [
p["version"] for p in plugins if p["id"] == plugin_id
][0]
plugin_info["uploadDate"] = time.strftime("%Y-%m-%d", time.localtime())
plugin_info["path"] = str(
self.plugins_dir / name / plugin_config["pluginVersion"]
)
# 保存插件信息
with open(self.plugins_info_file, "w") as f:
json.dump(plugins, f)
# 清理临时文件
os.unlink(tmp_filepath)
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): def extract_plugin(self, plugin_file, plugin_name):
"""解压插件""" """解压插件"""
try: try:
with zipfile.ZipFile(plugin_file, 'r') as zf: with zipfile.ZipFile(plugin_file, "r") as zf:
# 获取插件名称 # 获取插件名称
plugin_dir = self.plugins_dir / plugin_name plugin_dir = self.plugins_dir / plugin_name
@@ -112,8 +297,8 @@ class WebviewBridge(QObject):
if not html_files: if not html_files:
return False, "插件中未找到HTML文件" return False, "插件中未找到HTML文件"
index_file = html_files[0] index_file = html_files[0]
return True, "" return True, str(index_file.absolute())
except Exception as e: except Exception as e:
return False, str(e) return False, str(e)
@@ -121,8 +306,8 @@ class WebviewBridge(QObject):
def list_plugins(self): def list_plugins(self):
"""获取插件列表""" """获取插件列表"""
try: try:
if self.plugins_info_file.exists(): if os.path.exists(self.plugins_info_file):
with open(self.plugins_info_file, 'r') as f: with open(self.plugins_info_file, "r") as f:
plugins = json.load(f) plugins = json.load(f)
return json.dumps({"success": True, "plugins": plugins}) return json.dumps({"success": True, "plugins": plugins})
else: else:
@@ -130,17 +315,18 @@ class WebviewBridge(QObject):
except Exception as e: except Exception as e:
# 确保任何异常都返回有效的JSON # 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)}) return json.dumps({"success": False, "error": str(e)})
@Slot(int, result=str) @Slot(str, result=str)
def launch_plugin(self, plugin_id): def launch_plugin(self, plugin_id):
"""启动插件""" """启动插件"""
try: try:
if self.plugins_info_file.exists(): if os.path.exists(self.plugins_info_file):
with open(self.plugins_info_file, 'r') as f: with open(self.plugins_info_file, "r") as f:
plugins = json.load(f) plugins = json.load(f)
# 查找插件 # 查找插件
plugin = next((p for p in plugins if p["id"] == plugin_id), None) plugin = next((p for p in plugins if p["id"] == plugin_id), None)
print(f"Launching plugin: {plugin}")
if plugin: if plugin:
# 构建插件URL # 构建插件URL
plugin_path = Path(plugin["path"]) plugin_path = Path(plugin["path"])
@@ -150,10 +336,13 @@ class WebviewBridge(QObject):
index_file = html_files[0] index_file = html_files[0]
url = QUrl.fromLocalFile(str(index_file.absolute())) url = QUrl.fromLocalFile(str(index_file.absolute()))
print(f"Launching plugin: {url.toString()}") print(f"Launching plugin: {url.toString()}")
print(f"Plugin path: {plugin_path}")
self.webview.load(url) self.webview.load(url)
return json.dumps({"success": True, "url": url.toString()}) return json.dumps({"success": True, "url": url.toString()})
else: else:
return json.dumps({"success": False, "error": "插件入口文件未找到"}) return json.dumps(
{"success": False, "error": "插件入口文件未找到"}
)
else: else:
return json.dumps({"success": False, "error": "插件未找到"}) return json.dumps({"success": False, "error": "插件未找到"})
else: else:
@@ -161,7 +350,7 @@ class WebviewBridge(QObject):
except Exception as e: except Exception as e:
# 确保任何异常都返回有效的JSON # 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)}) return json.dumps({"success": False, "error": str(e)})
@Slot(str, result=str) @Slot(str, result=str)
def save_settings(self, settings_json): def save_settings(self, settings_json):
"""保存设置""" """保存设置"""
@@ -173,14 +362,17 @@ class WebviewBridge(QObject):
except Exception as e: except Exception as e:
# 确保任何异常都返回有效的JSON # 确保任何异常都返回有效的JSON
return json.dumps({"success": False, "error": str(e)}) return json.dumps({"success": False, "error": str(e)})
class WebviewController(QObject): class WebviewController(QObject):
"""Webview控制器类,负责管理Webview的逻辑""" """Webview控制器类,负责管理Webview的逻辑"""
def __init__(self, web_engine_page, web_webview, parent=None): def __init__(self, web_engine_page, web_webview, parent=None):
super().__init__(parent) super().__init__(parent)
self.page = web_engine_page self.page = web_engine_page
self.bridge = WebviewBridge(web_webview) self.bridge = WebviewBridge(web_webview)
self.queue_sqlite_operations = WebViewQueueSqliteOperations(web_webview)
self.channel = QWebChannel() self.channel = QWebChannel()
self.channel.registerObject('py', self.bridge) self.channel.registerObject("entrance", self.bridge)
self.channel.registerObject("queueSqlite", self.queue_sqlite_operations)
self.page.setWebChannel(self.channel) self.page.setWebChannel(self.channel)
+53 -121
View File
@@ -2,12 +2,21 @@
import sys import sys
import os import os
from pathlib import Path 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 PySide6.QtWidgets import (
QFrame,
QVBoxLayout,
QMenu,
QApplication,
QWidget,
QPushButton,
QHBoxLayout,
)
from PySide6.QtCore import QPoint, Qt, QUrl
from qframelesswindow.webengine import FramelessWebEngineView
from .entrance import WebviewController from .entrance import WebviewController
from ...config import config
class DevToolsWindow(FramelessWebEngineView): class DevToolsWindow(FramelessWebEngineView):
@@ -16,6 +25,7 @@ class DevToolsWindow(FramelessWebEngineView):
self.setWindowTitle("开发者工具") self.setWindowTitle("开发者工具")
self.resize(600, 600) self.resize(600, 600)
class DevToolsView(QWidget): class DevToolsView(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -25,6 +35,7 @@ class DevToolsView(QWidget):
# 设置窗口大小不可变 # 设置窗口大小不可变
self.setFixedSize(self.size()) self.setFixedSize(self.size())
class WebViewInterface(QFrame): class WebViewInterface(QFrame):
def __init__(self, parent=None): def __init__(self, parent=None):
@@ -33,37 +44,42 @@ class WebViewInterface(QFrame):
self.dev_tools_view = None self.dev_tools_view = None
# 添加连接状态标志 # 添加连接状态标志
self.is_local_connection = config.get("project", {}).get("is-production", True) self.is_local_connection = config.get("project", {}).get("is-production", True)
# 插件目录 # 插件目录
self.plugins_dir = Path(os.getcwd()) / "plugins" self.plugins_dir = Path(os.getcwd()) / "plugins"
self.plugins_dir.mkdir(exist_ok=True) self.plugins_dir.mkdir(exist_ok=True)
self.webView = FramelessWebEngineView(self) self.webView = FramelessWebEngineView(self)
self.html_path = os.getcwd().replace("\\", '/') + '/resources/webview/entrance/dist/index.html' self.html_path = (
self.remote_url = "http://localhost:8080/" os.getcwd().replace("\\", "/")
+ "/resources/webview/entrance/dist/index.html"
)
self.remote_url = config.get("project", {}).get(
"remote-url", "http://localhost:8080/"
)
# 创建WebviewController实例 # 创建WebviewController实例
self.page = self.webView.page() self.page = self.webView.page()
self.controller = WebviewController(self.page, self.webView) self.controller = WebviewController(self.page, self.webView)
# 加载初始页面 # 加载初始页面
self.load_current_page() self.load_current_page()
# 创建导航按钮 # 创建导航按钮
self.back_button = QPushButton("⬅️", self) self.back_button = QPushButton("⬅️", self)
self.back_button.clicked.connect(self.webView.back) self.back_button.clicked.connect(self.webView.back)
self.back_button.setFixedSize(40, 30) self.back_button.setFixedSize(40, 30)
self.back_button.setEnabled(False) self.back_button.setEnabled(False)
self.forward_button = QPushButton("➡️", self) self.forward_button = QPushButton("➡️", self)
self.forward_button.clicked.connect(self.webView.forward) self.forward_button.clicked.connect(self.webView.forward)
self.forward_button.setFixedSize(40, 30) self.forward_button.setFixedSize(40, 30)
self.forward_button.setEnabled(False) self.forward_button.setEnabled(False)
self.reload_button = QPushButton("🔃", self) self.reload_button = QPushButton("🔃", self)
self.reload_button.clicked.connect(self.webView.reload) self.reload_button.clicked.connect(self.webView.reload)
self.reload_button.setFixedSize(40, 30) self.reload_button.setFixedSize(40, 30)
self.home_button = QPushButton("🏠", self) self.home_button = QPushButton("🏠", self)
self.home_button.clicked.connect(self.go_home) self.home_button.clicked.connect(self.go_home)
self.home_button.setFixedSize(40, 30) self.home_button.setFixedSize(40, 30)
@@ -87,7 +103,7 @@ class WebViewInterface(QFrame):
self.webView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.webView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.webView.loadFinished.connect(self.on_load_finished) self.webView.loadFinished.connect(self.on_load_finished)
self.webView.customContextMenuRequested.connect(self.show_context_menu) self.webView.customContextMenuRequested.connect(self.show_context_menu)
# 应用样式 # 应用样式
self.apply_styles() self.apply_styles()
@@ -114,8 +130,10 @@ class WebViewInterface(QFrame):
color: #adb5bd; color: #adb5bd;
} }
""" """
nav_button_style = button_style + """ nav_button_style = (
button_style
+ """
QPushButton { QPushButton {
background-color: #6c757d; background-color: #6c757d;
} }
@@ -126,8 +144,11 @@ class WebViewInterface(QFrame):
background-color: #495057; background-color: #495057;
} }
""" """
)
toggle_button_style = button_style + """
toggle_button_style = (
button_style
+ """
QPushButton { QPushButton {
background-color: #0d6efd; background-color: #0d6efd;
} }
@@ -138,7 +159,8 @@ class WebViewInterface(QFrame):
background-color: #0a58ca; background-color: #0a58ca;
} }
""" """
)
self.back_button.setStyleSheet(nav_button_style) self.back_button.setStyleSheet(nav_button_style)
self.forward_button.setStyleSheet(nav_button_style) self.forward_button.setStyleSheet(nav_button_style)
self.reload_button.setStyleSheet(nav_button_style) self.reload_button.setStyleSheet(nav_button_style)
@@ -178,35 +200,35 @@ class WebViewInterface(QFrame):
"""显示自定义右键菜单""" """显示自定义右键菜单"""
# 创建上下文菜单 # 创建上下文菜单
menu = QMenu(self) menu = QMenu(self)
# 添加标准浏览器动作 # 添加标准浏览器动作
back_action = menu.addAction("后退") back_action = menu.addAction("后退")
back_action.triggered.connect(self.webView.back) back_action.triggered.connect(self.webView.back)
forward_action = menu.addAction("前进") forward_action = menu.addAction("前进")
forward_action.triggered.connect(self.webView.forward) forward_action.triggered.connect(self.webView.forward)
reload_action = menu.addAction("重新加载") reload_action = menu.addAction("重新加载")
reload_action.triggered.connect(self.webView.reload) reload_action.triggered.connect(self.webView.reload)
menu.addSeparator() menu.addSeparator()
# 添加开发者工具相关动作 # 添加开发者工具相关动作
dev_tools_action = menu.addAction("打开开发者工具") dev_tools_action = menu.addAction("打开开发者工具")
dev_tools_action.triggered.connect(self.toggle_dev_tools) dev_tools_action.triggered.connect(self.toggle_dev_tools)
menu.addSeparator() menu.addSeparator()
menu.addSeparator() menu.addSeparator()
# 在鼠标位置显示菜单 # 在鼠标位置显示菜单
menu.exec(self.webView.mapToGlobal(pos)) menu.exec(self.webView.mapToGlobal(pos))
def toggle_dev_tools(self): def toggle_dev_tools(self):
"""切换开发者工具窗口显示状态""" """切换开发者工具窗口显示状态"""
if not self.dev_tools_view: if not self.dev_tools_view:
self.init_dev_tools() self.init_dev_tools()
if self.dev_tools_view is None: if self.dev_tools_view is None:
return return
@@ -217,97 +239,7 @@ class WebViewInterface(QFrame):
self.dev_tools_view.show() self.dev_tools_view.show()
self.dev_tools_view.dev_tools_window.show() self.dev_tools_view.dev_tools_window.show()
self.dev_tools_view.dev_tools_window.raise_() # 将窗口置于最前 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): def closeEvent(self, event):
"""关闭时清理资源""" """关闭时清理资源"""
# 停止controller中的定时器 # 停止controller中的定时器
@@ -322,4 +254,4 @@ if __name__ == "__main__":
window = WebViewInterface() window = WebViewInterface()
window.resize(1000, 700) window.resize(1000, 700)
window.show() window.show()
sys.exit(app.exec()) sys.exit(app.exec())
+8 -11
View File
@@ -1,21 +1,18 @@
import sys import sys
import os import os
from pathlib import Path
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from app.view import Window from app.view import Window
from app.scheduler_manager import scheduler from app.scheduler_manager import scheduler
def resource_path(relative_path):
"""获取资源的绝对路径。用于PyInstaller/Nuitka打包后定位资源文件。"""
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path) # type: ignore
return os.path.join(os.path.abspath("."), relative_path)
def main(): def main():
# # 设置插件路径 - 关键修复 # 设置基础路径
# if getattr(sys, 'frozen', False): base_dir = os.path.abspath(".")
# # 打包后的路径
# base_dir = Path(sys._MEIPASS)
# else:
# # 开发环境路径
# base_dir = Path(__file__).parent
# # 设置Qt插件路径
# os.environ['QT_PLUGIN_PATH'] = str(base_dir / 'plugins')
# 启动队列调度器 # 启动队列调度器
scheduler.start_queue_scheduler() scheduler.start_queue_scheduler()
+1893 -292
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -14,6 +14,7 @@ dependencies = [
] ]
# 是否是生产环境 # 是否是生产环境
is-production = false is-production = false
remote-url = "http://localhost:8080/"
[[tool.uv.index]] [[tool.uv.index]]
default = true default = true
File diff suppressed because it is too large Load Diff
+2
View File
@@ -10,6 +10,8 @@
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"qwebchannel": "^6.2.0", "qwebchannel": "^6.2.0",
"sass": "^1.92.1",
"sass-loader": "^16.0.5",
"vue": "^3.2.13" "vue": "^3.2.13"
}, },
"devDependencies": { "devDependencies": {
@@ -0,0 +1,7 @@
{
"pluginName": "WebView Entrance",
"pluginId": "com.webview.entrance",
"pluginDescription": "This plugin is used to show the entrance of the webview.",
"pluginVersion": "1.0.0",
"pluginAuthor": "chakcy"
}
File diff suppressed because it is too large Load Diff
@@ -1,58 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+24
View File
@@ -0,0 +1,24 @@
# new_plugin
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "new_plugin",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "node scripts/copy-plugin-config.js && vue-cli-service serve",
"build": "node scripts/copy-plugin-config.js && vue-cli-service build",
"lint": "node scripts/copy-plugin-config.js && vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"qwebchannel": "^6.2.0",
"vue": "^3.2.13"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
@@ -0,0 +1,7 @@
{
"pluginName": "new plugin",
"pluginId": "com.example.newplugin",
"pluginDescription": "This is a new plugin",
"pluginVersion": "1.0.0",
"pluginAuthor": "chakcy"
}
@@ -0,0 +1,22 @@
const fs = require('fs');
const path = require('path');
// 定义源文件和目标文件路径
const srcConfigPath = path.resolve(__dirname, '../src/config/plugin.config.json');
const publicDirPath = path.resolve(__dirname, '../public');
const publicConfigPath = path.resolve(publicDirPath, 'plugin.config.json');
// 检查源文件是否存在
if (fs.existsSync(srcConfigPath)) {
// 确保public目录存在
if (!fs.existsSync(publicDirPath)) {
fs.mkdirSync(publicDirPath, { recursive: true });
}
// 复制文件到public目录
fs.copyFileSync(srcConfigPath, publicConfigPath);
console.log('Successfully copied plugin.config.json to public directory');
} else {
console.error('Source plugin.config.json not found at:', srcConfigPath);
process.exit(1);
}
+26
View File
@@ -0,0 +1,26 @@
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</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: 60px;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@@ -0,0 +1,57 @@
<template>
<div>
<h2>任务状态</h2>
{{ queueStore.queueData }}
<div v-if="queueStore.queueData.taskStatus">
<p>当前状态: {{ queueStore.queueData.taskStatus }}</p>
<p>处理结果: {{ queueStore.queueData.taskResult }}</p>
</div>
<button @click="sendTask">发送任务</button>
</div>
</template>
<script setup>
import { useQueueStore } from '@/stores/queue'
import { onMounted } from 'vue'
import { QWebChannel } from 'qwebchannel';
const queueStore = useQueueStore()
let queueSqlite = null;
// 定义全局函数用于更新状态
window.updateQueueState = (key, value) => {
try {
console.log('更新状态:', key, value);
queueStore.updateQueueData(key, value);
} catch (e) {
console.error('更新状态时出错:', e);
}
};
onMounted(() => {
if (window.qt && window.qt.webChannelTransport) {
new QWebChannel(window.qt.webChannelTransport, function (channel) {
queueSqlite = channel.objects.queueSqlite;
console.log("queueSqlite", queueSqlite)
});
}
});
// 这个函数会通过 QWebChannel 调用 Python 端代码
const sendTask = () => {
// 调用 Python 端的 send_message 方法
if (queueSqlite && queueSqlite.send_message) {
// 发从 message 以及 pinia store 名称
queueSqlite.send_message(
JSON.stringify({
"content": {"num": 1},
"destination": "test"
}),
"taskStatus"
)
}
}
// 当 Python 端执行回调并更新状态时,页面会自动显示最新状态
</script>
@@ -0,0 +1,7 @@
{
"pluginName": "new plugin",
"pluginId": "com.example.newplugin",
"pluginDescription": "This is a new plugin",
"pluginVersion": "1.0.0",
"pluginAuthor": "chakcy"
}
+16
View File
@@ -0,0 +1,16 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import { useQueueStore } from './stores/queue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')
window.queueStore = useQueueStore()
window.pluginConfig = await fetch('./plugin.config.json').then(res => res.json())
console.log('Loaded plugin config:', window.pluginConfig)
@@ -0,0 +1,6 @@
// src/stores/index.js 或 main.js 中
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
@@ -0,0 +1,18 @@
import { defineStore } from 'pinia'
export const useQueueStore = defineStore('queueSqlite', {
state: () => ({
// 存储队列相关状态
queueData: {},
// 其他状态...
}),
persist: true, // 开启持久化
actions: {
updateQueueData(key, value) {
this.queueData[key] = value
},
// 其他相关操作...
}
})
@@ -0,0 +1,5 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
publicPath: './'
})
+1
View File
@@ -8,4 +8,5 @@ python -m nuitka ^
--output-dir=output ^ --output-dir=output ^
--remove-output ^ --remove-output ^
--jobs=4 ^ --jobs=4 ^
--output-filename="pyside_queue_sqlite.exe" ^
main.py main.py
+24 -14
View File
@@ -1,28 +1,38 @@
import os import os
import sys import sys
def generate_markdown_from_py_files(directory, output_file): def generate_markdown_from_py_files(directory, output_file):
with open(output_file, 'w', encoding='utf-8') as md_file: with open(output_file, "w", encoding="utf-8") as md_file:
for root, dirs, files in os.walk(directory): for root, dirs, files in os.walk(directory):
# 排除 venv 目录 # 排除 venv 目录
dirs[:] = [d for d in dirs if d != '.venv'] dirs[:] = [d for d in dirs if d != ".venv"]
dirs[:] = [d for d in dirs if d != '.vscode'] dirs[:] = [d for d in dirs if d != ".vscode"]
dirs[:] = [d for d in dirs if d != 'scripts'] dirs[:] = [d for d in dirs if d != "scripts"]
dirs[:] = [d for d in dirs if d != 'build'] dirs[:] = [d for d in dirs if d != "build"]
dirs[:] = [d for d in dirs if d != 'lib'] dirs[:] = [d for d in dirs if d != "lib"]
dirs[:] = [d for d in dirs if d != "dist"]
dirs[:] = [d for d in dirs if d != "node_modules"]
dirs[:] = [d for d in dirs if d != "plugins"]
for file in files: for file in files:
if file.endswith('.py') or file.endswith('.rs'): if (
file.endswith(".py")
or file.endswith(".rs")
or file.endswith(".js")
or file.endswith(".vue")
):
file_path = os.path.join(root, file) file_path = os.path.join(root, file)
md_file.write(f'`{file_path}`\n') md_file.write(f"`{file_path}`\n")
md_file.write('```python\n') md_file.write("```python\n")
with open(file_path, 'r', encoding='utf-8') as py_file: with open(file_path, "r", encoding="utf-8") as py_file:
md_file.write(py_file.read()) md_file.write(py_file.read())
md_file.write('\n```\n\n') md_file.write("\n```\n\n")
if __name__ == "__main__": if __name__ == "__main__":
# 指定目录和输出文件名 # 指定目录和输出文件名
target_directory = sys.argv[1] # 替换为你的目标目录 target_directory = sys.argv[1] # 替换为你的目标目录
output_markdown_file = 'output.md' # 输出的 Markdown 文件名 output_markdown_file = "output.md" # 输出的 Markdown 文件名
generate_markdown_from_py_files(target_directory, output_markdown_file) generate_markdown_from_py_files(target_directory, output_markdown_file)
print(f'Markdown 文件已生成:{output_markdown_file}') print(f"Markdown 文件已生成:{output_markdown_file}")