2025-09-06 16:06:30 +08:00

32 KiB
Raw Blame History

./main.py

import sys
import os
from pathlib import Path
from PySide6.QtWidgets import QApplication
from app.view import Window
from app.scheduler_manager import scheduler

def main():
    # # 设置插件路径 - 关键修复
    # if getattr(sys, 'frozen', False):
    #     # 打包后的路径
    #     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()

    # 创建Qt应用
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    window.setMicaEffectEnabled(True)
    
    # 启动事件循环
    exit_code = app.exec()

    # 停止队列调度器
    scheduler.stop_queue_scheduler()
    
    sys.exit(exit_code)

if __name__ == "__main__":
    main()

./app\__init__.py


./app\scheduler_manager\__init__.py

from queue_sqlite.scheduler import QueueScheduler
from .students.add_student import add_student 
from .students.students_listen import students


scheduler = QueueScheduler()

__all__ = [
    "scheduler",  
    "add_student",
    "students"
]

./app\scheduler_manager\students\add_student.py

from queue_sqlite.mounter.task_mounter import TaskMounter
from queue_sqlite.model import MessageItem
import time


@TaskMounter.task(meta={"task_name": "add_student"})
def add_student(message_item: MessageItem):
    # create a new instance of the AddStudentDilalog dialog
    # time.sleep(3)
    print("点击了添加按钮")
    return {"message": "学生添加成功"}

./app\scheduler_manager\students\students_listen.py

from queue_sqlite.mounter.listen_mounter import ListenMounter
from ...signal import listen_signals
import json


@ListenMounter.listener()
def students(str_students_list: str):
    print("students signal received")
    listen_signals.listening_students_signal.emit(json.loads(str_students_list))

./app\signal\global_signals.py

from PySide6.QtCore import Signal, QObject


class GlobalSignals(QObject):

    show_add_dialog_signal = Signal(dict)

__all__ = ["GlobalSignals"]

./app\signal\listen_signals.py

from PySide6.QtCore import Signal, QObject


class ListenSignals(QObject):

    listening_students_signal = Signal(list)

__all__ = ["ListenSignals"]

./app\signal\__init__.py

from .global_signals import GlobalSignals
from .listen_signals import ListenSignals

global_signals = GlobalSignals()
listen_signals = ListenSignals()

__all__ = ["global_signals"]

./app\style\button_style.py

BUTTON_STYLE = """
QPushButton {
    border: none;
    padding: 5px 10px;
    font-family: 'Segoe UI', 'Microsoft YaHei';
    font-size: 14px;
    color: white;
    border-radius: 5px;
}
QPushButton:hover {
    background-color: rgba(255, 255, 255, 0.1);
}
QPushButton:pressed{
    background-color: rgba(255, 255, 255, 0.2);
}
"""

ADD_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #0d6efd;
}
QPushButton:hover {
    background-color: #0b5ed7;
}
QPushButton:pressed{
    background-color: #0a58ca;
}
"""

DELETE_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #dc3545;
}
QPushButton:hover {
    background-color: #bb2d3b;
}
QPushButton:pressed{
    background-color: #b02a37;
"""

BATCH_DELETE_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #fd7e14;
}
QPushButton:hover {
    background-color: #e96b10;
}
QPushButton:pressed{
    background-color: #dc680f;
}
"""

UPDATE_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #198754;
}
QPushButton:hover {
     background-color: #157347;
}
QPushButton:pressed{
    background-color: #146c43;
}
"""

IMPORT_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #6f42c1;
}
QPushButton:hover {
    background-color: #5936a2;
}
QPushButton:pressed{
    background-color: #4a2d8e;
}
"""

EXPORT_BUTTON_STYLE = BUTTON_STYLE + """
QPushButton {
    background-color: #20c997;
}
QPushButton:hover {
    background-color: #1aa179;
}
QPushButton:pressed{
    background-color: #198b6d;
}
"""

./app\style\__init__.py


./app\view\__init__.py

# coding:utf-8
from PySide6.QtCore import QUrl, QSize, QEventLoop, QTimer
from PySide6.QtGui import QIcon, QDesktopServices
from qfluentwidgets import (NavigationItemPosition, MessageBox, NavigationAvatarWidget,  SplitFluentWindow, SplashScreen)
from qfluentwidgets import FluentIcon as FIF

from .students.student_interface import StudentInterface
from .video.video_interface import VideoInterface
from .camera.camera_interface import CameraInterface
from .web_view.web_view_interface import WebViewInterface
from PySide6.QtWidgets import QApplication


class Window(SplitFluentWindow):
    def __init__(self):
        super().__init__()

        self.studemt_interface = StudentInterface()
        self.video_interface = VideoInterface()
        self.camera_interface = CameraInterface()
        self.web_view_interface = WebViewInterface(self)
        self.initNavigation()
        self.resize(900, 700)
        self.setWindowIcon(QIcon('./resources/images/logo.png'))

        desktop = QApplication.screens()[0].availableGeometry()
        w, h = desktop.width(), desktop.height()
        self.move(w//2 - self.width()//2, h//2 - self.height()//2)

        self.splashScreen = SplashScreen(self.windowIcon(), self)
        self.splashScreen.setIconSize(QSize(102, 102))
        self.show()
        self.createSubInterface()
        self.splashScreen.finish()
    
    def createSubInterface(self):
        loop = QEventLoop(self)
        QTimer.singleShot(2000, loop.quit)
        loop.exec()

    def initNavigation(self):
        self.addSubInterface(self.studemt_interface, FIF.RINGER, "Students")
        self.addSubInterface(self.video_interface, FIF.VIDEO, "Video")
        self.addSubInterface(self.camera_interface, FIF.CAMERA, "Camera")
        self.addSubInterface(self.web_view_interface, FIF.FIT_PAGE, "Web View")

        self.navigationInterface.addWidget(
            routeKey="Info",
            widget=NavigationAvatarWidget('Info', 'resource/images/logo.png'),
            onClick=self.showMessageBox,
            position=NavigationItemPosition.BOTTOM,
        )

        self.navigationInterface.addItem(
            routeKey='settingInterface',
            icon=FIF.SETTING,
            text='设置',
            position=NavigationItemPosition.BOTTOM,
        )

        self.navigationInterface.setExpandWidth(280)

    def showMessageBox(self):
        w = MessageBox(
            '支持作者🥰',
            '个人开发不易,如果这个项目帮助到了您,可以考虑请作者喝一瓶快乐水🥤。您的支持就是作者开发和维护项目的动力🚀',
            self
        )
        w.yesButton.setText('来啦老弟')
        w.cancelButton.setText('下次一定')

        if w.exec():
            QDesktopServices.openUrl(QUrl("https://afdian.net/a/zhiyiYo"))

./app\view\camera\camera_interface.py

import sys
import cv2
from PySide6.QtCore import Qt, QTimer, Signal, QObject
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget, QMessageBox
from qfluentwidgets import PushButton


class VideoStream(QObject):
    new_frame = Signal(QImage)
    error_occurred = Signal(str)

    def __init__(self, rtsp_url):
        super().__init__()
        self.stream_url = rtsp_url  # 直接使用RTSP URL
        self.cap = None
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_frame)
        self.retry_count = 0
        self.max_retry = 5
        
        self.open_stream()
    
    def open_stream(self):
        """尝试打开RTSP视频流"""
        if self.cap:
            self.cap.release()
            
        # 创建VideoCapture对象并设置RTSP参数
        self.cap = cv2.VideoCapture(self.stream_url)
        
        # 关键设置使用TCP传输避免UDP丢包问题设置超时和缓冲区
        # 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_BUFFERSIZE, 1)  # 最小化缓冲区
        
        # 打开RTSP流
        self.cap.open(self.stream_url)
        
        if not self.cap.isOpened():
            self.retry_count += 1
            if self.retry_count <= self.max_retry:
                QTimer.singleShot(2000, self.open_stream)  # 2秒后重试
            else:
                self.error_occurred.emit(f"无法打开RTSP视频流: {self.stream_url}")
        else:
            self.retry_count = 0
            self.timer.start(30)  # 约30fps
    
    def update_frame(self):
        if not self.cap or not self.cap.isOpened():
            return
            
        ret, frame = self.cap.read()
        if ret:
            # 转换为黑白图像
            # gray_image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            # h, w = gray_image.shape
            # qt_image = QImage(gray_image.data, w, h, w, QImage.Format.Format_Grayscale8)

            # 转换为RGB图像
            rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            h, w, ch = rgb_image.shape
            qt_image = QImage(rgb_image.data, w, h, w * ch, QImage.Format.Format_RGB888)

            self.new_frame.emit(qt_image)
        else:
            # 读取失败时重新连接
            self.open_stream()
    
    def stop(self):
        self.timer.stop()
        if self.cap:
            self.cap.release()

class CameraInterface(QWidget):
    def __init__(self, rtsp_url="rtsp://127.0.0.1:8554/stream"):
        super().__init__()
        self.setObjectName("camera_interface")
        self.setWindowTitle("RTSP视频流接收器")
        self.resize(800, 600)
        
        # 创建UI
        self.label = QLabel("等待RTSP视频流...")
        self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.label.setStyleSheet("font-size: 20px; color: #888;")
        
        # 创建按钮
        self.listen_button = PushButton("开始监听RTSP")
        self.listen_button.clicked.connect(lambda: self.start_listen(rtsp_url))

        layout = QVBoxLayout()
        layout.setContentsMargins(20, 50, 20, 20)
        layout.addWidget(self.label)
        layout.addWidget(self.listen_button)
        self.setLayout(layout)
        
    def start_listen(self, rtsp_url):
        self.listen_button.setEnabled(False)
        self.label.setText(f"正在连接: {rtsp_url}...")
        # 创建视频流
        self.stream = VideoStream(rtsp_url)
        self.stream.new_frame.connect(self.display_frame)
        self.stream.error_occurred.connect(self.display_error)
    
    def display_frame(self, image):
        pixmap = QPixmap.fromImage(image)
        self.label.setPixmap(pixmap.scaled(
            self.label.width(), 
            self.label.height(),
            Qt.AspectRatioMode.KeepAspectRatio,
            Qt.TransformationMode.SmoothTransformation
        ))
    
    def display_error(self, message):
        QMessageBox.critical(self, "RTSP视频流错误", message)
        self.label.setText(f"错误: {message}")
        self.listen_button.setEnabled(True)  # 出错时重新启用按钮
    
    def closeEvent(self, event):
        if hasattr(self, 'stream') and self.stream:
            self.stream.stop()
        super().closeEvent(event)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    
    # 使用命令行参数指定RTSP URL
    rtsp_url = "rtsp://127.0.0.1:8554/stream"  # 默认URL
    if len(sys.argv) > 1:
        rtsp_url = sys.argv[1]
    
    player = CameraInterface(rtsp_url)
    player.show()
    sys.exit(app.exec())
    

./app\view\camera\__init__.py


./app\view\students\student_dilalog.py

from qfluentwidgets import MessageBoxBase, LineEdit, ComboBox, SubtitleLabel
from PySide6.QtWidgets import QGridLayout, QLabel
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont


class BaseStudentDilalog(MessageBoxBase):
    def __init__(self, title, parent=None):
        super().__init__(parent)
        self.title = title
        self.setup_ui()

    def setup_ui(self):
        self.titleLabel = SubtitleLabel(self.title, self)
        self.viewLayout.addWidget(self.titleLabel)
        self.viewLayout.setAlignment(self.titleLabel, Qt.AlignmentFlag.AlignCenter)
        
        grid_layout = QGridLayout()
        self.viewLayout.addLayout(grid_layout)

        self.nameInput = LineEdit(self)
        self.numberInput = LineEdit(self)
        self.genderCombo = ComboBox(self)
        self.genderCombo.addItems(["男", "女"])
        self.classCombo = ComboBox(self)
        self.chineseInput = LineEdit(self)
        self.mathInput = LineEdit(self)
        self.englishInput = LineEdit(self)

        fields = [
            ("姓名: ", self.nameInput),
            ("学号: ", self.numberInput),
            ("性别: ", self.genderCombo),
            ("班级: ", self.classCombo),
            ("语文: ", self.chineseInput),
            ("数学: ", self.mathInput),
            ("英语: ", self.englishInput),
        ]

        for row, (label_text, widget) in enumerate(fields):
            label_widget = QLabel(label_text)
            font = QFont("微软雅黑")
            font.setBold(True)
            label_widget.setFont(font)
            grid_layout.addWidget(label_widget, row, 0)
            grid_layout.addWidget(widget, row, 1)

        grid_layout.setColumnStretch(1, 1)
        grid_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)

        self.yesButton.setText('确定')
        self.cancelButton.setText('取消')

        for widget in [field[1] for field in fields]:
            widget.setMinimumWidth(200)

        self.nameInput.setFocus()

        self.yesButton.setDefault(False)
        self.yesButton.setAutoDefault(False)


class AddStudentDilalog(BaseStudentDilalog):
    def __init__(self, parent=None):
        super().__init__("添加学生", parent)
        self.student_id = None
        self.yesButton.setText('添加')

./app\view\students\student_interface.py

import json

from PySide6.QtGui import QIcon
from PySide6.QtCore import Slot
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QHeaderView, QCheckBox, QTableWidgetItem

from queue_sqlite.model import MessageItem

from qfluentwidgets import CardWidget, PushButton, SearchLineEdit, TableWidget, setCustomStyleSheet

from .student_dilalog import AddStudentDilalog
from ...style.button_style import ADD_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE
from ...signal import global_signals
from ...signal import listen_signals
from ...scheduler_manager import scheduler


class StudentInterface(QWidget):
    def __init__(self):
        super().__init__()
        self.setObjectName("student_interface")
        self.students = []
        self.setup_ui()
        
        self.create_signal_slot()
        self.load_data()
        self.populate_table(self.students)

    def create_signal_slot(self):
        global_signals.show_add_dialog_signal.connect(self.show_add_student_dialog)
        listen_signals.listening_students_signal.connect(self.populate_table)

    def setup_ui(self):
        # 设置 title
        self.setWindowTitle("学生管理")
        layout = QVBoxLayout(self)
        layout.setContentsMargins(20, 50, 20, 20) # (左, 上, 右, 下)
        card_widget = CardWidget(self)
        buttons_layout = QHBoxLayout(card_widget)

        self.addButton = PushButton("新增", self)
        setCustomStyleSheet(self.addButton, ADD_BUTTON_STYLE, ADD_BUTTON_STYLE)
        self.addButton.clicked.connect(self.add_student)
        
        self.searchInput = SearchLineEdit(self)
        self.searchInput.setPlaceholderText("搜索学生姓名或学号...")
        self.searchInput.setFixedWidth(500)
        self.batchDeleteButton = PushButton("批量删除", self)
        setCustomStyleSheet(self.batchDeleteButton, BATCH_DELETE_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE)

        buttons_layout.addWidget(self.addButton)
        buttons_layout.addWidget(self.searchInput)
        buttons_layout.addStretch(2)
        buttons_layout.addWidget(self.batchDeleteButton)

        layout.addWidget(card_widget)

        self.table_widget = TableWidget(self)
        self.table_widget.setBorderRadius(8) # 设置边框圆角
        self.table_widget.setBorderVisible(True) # 设置表格边框可见
        table_labels = ["", "学生ID", "姓名", "学号", "性别", "班级", "语文", "数学", "英语", "总分", "操作"]
        self.table_widget.setColumnCount(len(table_labels))
        self.table_widget.setHorizontalHeaderLabels(table_labels)
        self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)

        layout.addWidget(self.table_widget)

        self.setStyleSheet("StudentInterface {background: white}")
        self.setWindowIcon(QIcon('./resources/images/logo.png'))

    def load_data(self):
        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_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))

    @Slot(list)
    def populate_table(self, students: list):
        print(f"Received students: {students}")
        self.table_widget.setRowCount(len(students))
        for row, student_info in enumerate(students):
            self.setup_table_row(row, student_info)

    def setup_table_row(self, row, student_info):
        checkbox = QCheckBox()
        checkbox.setStyleSheet("margin: 10px")
        self.table_widget.setCellWidget(row, 0, checkbox)
        for col, key in enumerate(self.students[0].keys()):
            value = student_info.get(key, "")
            if key == "gender":
                value = "男" if value == 1 else "女" if value == 2 else "未知"
            item = QTableWidgetItem(str(value))
            self.table_widget.setItem(row, col+1, item)

    @Slot(dict)
    def show_add_student_dialog(self, message_dict: dict):
        message = MessageItem.from_dict(message_dict)
        print(f"Received message: {message}")
        print("Creating and showing AddStudentDialog")
        self.addButton.setEnabled(True)
        add_student_dialog = AddStudentDilalog(self)
        if add_student_dialog.exec():
            print("用户点击了添加按钮")
        else:
            print("用户点击了取消按钮")

    def add_student_callback(self, message: MessageItem):
        global_signals.show_add_dialog_signal.emit(message.to_dict())

    def add_student(self):
        self.addButton.setEnabled(False)
        scheduler.send_message(MessageItem(content={}, destination="add_student"), self.add_student_callback)

./app\view\students\__init__.py


./app\view\video\video_interface.py

from PySide6.QtCore import QUrl, Qt
from PySide6.QtGui import QFont  # 导入QFont
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QFileDialog, QHBoxLayout
from qfluentwidgets import setTheme, Theme, FluentIcon, PushButton, PrimaryPushButton
from qfluentwidgets.multimedia import VideoWidget


class VideoInterface(QWidget):
    def __init__(self):
        super().__init__()
        from PySide6.QtMultimedia import QMediaPlayer
        self.player = QMediaPlayer()
        self.setObjectName("video_interface")
        self.setWindowTitle("Fluent Video Player")
        self.resize(900, 600)
        
        # 设置主题
        # setTheme(Theme.DARK)
        
        # 创建主布局
        self.main_layout = QVBoxLayout(self)
        self.main_layout.setContentsMargins(20, 50, 20, 20)
        self.main_layout.setSpacing(15)
        
        # 创建标题标签 - 修复字体设置
        self.title_label = QLabel("Fluent Video Player")
        self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # 正确设置字体使用QFont.Bold表示粗体
        title_font = QFont()
        title_font.setPointSize(24)
        title_font.setBold(True)  # 使用setBold方法设置粗体
        self.title_label.setFont(title_font)
        
        self.main_layout.addWidget(self.title_label)
        
        # 创建视频播放器
        self.video_widget = VideoWidget()
        self.video_widget.setMinimumHeight(400)
        self.main_layout.addWidget(self.video_widget, 1)
        
        # 创建控制面板
        self.control_layout = QHBoxLayout()
        self.control_layout.setContentsMargins(10, 10, 10, 10)
        self.control_layout.setSpacing(15)
        self.main_layout.addLayout(self.control_layout)
        
        # 添加控制按钮
        self.open_button = PushButton(FluentIcon.FOLDER_ADD, "打开视频")
        self.open_button.setFixedWidth(120)
        self.open_button.clicked.connect(self.open_video)
        self.control_layout.addWidget(self.open_button)
        
        
        # 添加状态标签 - 修复字体设置
        self.status_label = QLabel("准备就绪")
        self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        status_font = QFont()
        status_font.setPointSize(12)
        self.status_label.setFont(status_font)
        self.control_layout.addWidget(self.status_label)
        
        # 设置初始状态
        self.video_loaded = False
        self.is_fullscreen = False
        
        # 设置样式
        # self.setStyleSheet("""
        #     QWidget {
        #         background-color: #2b2b2b;
        #         color: #f0f0f0;
        #     }
        #     QLabel {
        #         padding: 5px;
        #     }
        # """)
        
    def open_video(self):
        """打开视频文件"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择视频文件", "", 
            "视频文件 (*.mp4 *.avi *.mkv *.mov *.flv *.wmv);;所有文件 (*.*)"
        )
        
        if file_path:
            # 设置视频URL
            self.video_widget.setVideo(QUrl.fromLocalFile(file_path))
            self.status_label.setText(f"已加载视频: {file_path.split('/')[-1]}")
            self.video_loaded = True
            
    def play_video(self):
        """播放视频"""
        if self.video_loaded:
            self.video_widget.play()
            self.status_label.setText("正在播放...")
            
    def pause_video(self):
        """暂停视频"""
        if self.video_loaded:
            self.video_widget.pause()
            self.status_label.setText("已暂停")
            
    def stop_video(self):
        """停止视频"""
        if self.video_loaded:
            self.video_widget.stop()
            self.status_label.setText("已停止")
            
    def toggle_fullscreen(self):
        """切换全屏模式"""
        if self.is_fullscreen:
            self.showNormal()
        else:
            self.showFullScreen()
            self.is_fullscreen = True
            

# if __name__ == "__main__":
#     app = QApplication(sys.argv)
    
#     # 设置应用程序样式
#     from qfluentwidgets import setThemeColor
#     setThemeColor('#28a9e0')
    
#     player = VideoPlayer()
#     player.show()
#     sys.exit(app.exec())

./app\view\video\__init__.py


./app\view\web_view\web_view_interface.py

# coding:utf-8
import sys
from PySide6.QtWidgets import QFrame, QVBoxLayout, QMenu, QApplication, QWidget
from qframelesswindow.webengine import FramelessWebEngineView
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtCore import QTimer, Slot, QPoint, Qt, QUrl
from PySide6.QtWebEngineCore import QWebEnginePage

import time
import os

class DevToolsWindow(FramelessWebEngineView):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setWindowTitle("开发者工具")
        self.resize(600, 600)

class DevToolsView(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setWindowTitle("开发者工具")
        self.dev_tools_window = DevToolsWindow(self)
        self.resize(600, 600)
        # 设置窗口大小不可变
        self.setFixedSize(self.size())

class WebViewInterface(QFrame):

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setObjectName("webViewInterface")
        self.dev_tools_view = None

        self.webView = FramelessWebEngineView(self)
        self.channel = QWebChannel()
        self.channel.registerObject('py', self)
        html_path = os.getcwd().replace("\\", '/') + '/resources/webview/test_webview/dist/index.html'
        # html_path = "http://localhost:8080/"
        self.webView.load(QUrl(html_path))
        self.page = self.webView.page()
        self.page.setWebChannel(self.channel)
        self.timer = QTimer(self)
        # self.timer.timeout.connect(self.send_time)
        self.timer.start(100)

        self.vBoxLayout = QVBoxLayout(self)
        self.vBoxLayout.setContentsMargins(0, 48, 0, 0)
        self.vBoxLayout.addWidget(self.webView)
        self.webView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.webView.loadFinished.connect(self.on_load_finished)
        self.webView.customContextMenuRequested.connect(self.show_context_menu)


    def on_load_finished(self, success):
        """页面加载完成后初始化开发者工具"""
        if success:
            # 预初始化开发者工具
            self.init_dev_tools()

    def init_dev_tools(self):
        """初始化开发者工具"""
        if not self.dev_tools_view:
            # self.dev_tools_window = DevToolsWindow()
            self.dev_tools_view = DevToolsView()
            self.page.setDevToolsPage(self.dev_tools_view.dev_tools_window.page())

    @Slot(str, result=str)
    def hello(self, message):
        """js调用python测试"""
        print('call received')
        return f'hello from python: {message}'

    def send_time(self):
        """python调用js测试"""
        self.page.runJavaScript(f'console.log(app_sendMessage)')

    def show_context_menu(self, pos: QPoint):
        """显示自定义右键菜单"""
        # 创建上下文菜单
        menu = QMenu(self)
        
        # 添加标准浏览器动作
        back_action = menu.addAction("后退")
        back_action.triggered.connect(self.webView.back)
        
        forward_action = menu.addAction("前进")
        forward_action.triggered.connect(self.webView.forward)
        
        reload_action = menu.addAction("重新加载")
        reload_action.triggered.connect(self.webView.reload)
        
        menu.addSeparator()
        
        # 添加开发者工具相关动作
        dev_tools_action = menu.addAction("打开开发者工具")
        dev_tools_action.triggered.connect(self.toggle_dev_tools)
        
        menu.addSeparator()
        
        # 在鼠标位置显示菜单
        menu.exec(self.webView.mapToGlobal(pos))
        
    def toggle_dev_tools(self):
        """切换开发者工具窗口显示状态"""
        if not self.dev_tools_view:
            self.init_dev_tools()
        
        if self.dev_tools_view is None:
            return

        if self.dev_tools_view.dev_tools_window.isVisible():
            self.dev_tools_view.hide()
            self.dev_tools_view.dev_tools_window.hide()
        else:
            self.dev_tools_view.show()
            self.dev_tools_view.dev_tools_window.show()
            self.dev_tools_view.dev_tools_window.raise_()  # 将窗口置于最前
    
    def closeEvent(self, event):
        """关闭时清理资源"""
        if self.dev_tools_view:
            self.dev_tools_view.close()
        super().closeEvent(event)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = WebViewInterface()
    window.resize(1000, 700)
    window.show()
    sys.exit(app.exec())
    

./app\view\web_view\__init__.py


./resources\webview\test_webview\node_modules\flatted\python\flatted.py

# ISC License
#
# Copyright (c) 2018-2025, Andrea Giammarchi, @WebReflection
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.

import json as _json

class _Known:
    def __init__(self):
        self.key = []
        self.value = []

class _String:
    def __init__(self, value):
        self.value = value


def _array_keys(value):
    keys = []
    i = 0
    for _ in value:
        keys.append(i)
        i += 1
    return keys

def _object_keys(value):
    keys = []
    for key in value:
        keys.append(key)
    return keys

def _is_array(value):
    return isinstance(value, (list, tuple))

def _is_object(value):
    return isinstance(value, dict)

def _is_string(value):
    return isinstance(value, str)

def _index(known, input, value):
    input.append(value)
    index = str(len(input) - 1)
    known.key.append(value)
    known.value.append(index)
    return index

def _loop(keys, input, known, output):
    for key in keys:
        value = output[key]
        if isinstance(value, _String):
            _ref(key, input[int(value.value)], input, known, output)

    return output

def _ref(key, value, input, known, output):
    if _is_array(value) and value not in known:
        known.append(value)
        value = _loop(_array_keys(value), input, known, value)
    elif _is_object(value) and value not in known:
        known.append(value)
        value = _loop(_object_keys(value), input, known, value)

    output[key] = value

def _relate(known, input, value):
    if _is_string(value) or _is_array(value) or _is_object(value):
        try:
            return known.value[known.key.index(value)]
        except:
            return _index(known, input, value)

    return value

def _transform(known, input, value):
    if _is_array(value):
        output = []
        for val in value:
            output.append(_relate(known, input, val))
        return output

    if _is_object(value):
        obj = {}
        for key in value:
            obj[key] = _relate(known, input, value[key])
        return obj

    return value

def _wrap(value):
    if _is_string(value):
        return _String(value)

    if _is_array(value):
        i = 0
        for val in value:
            value[i] = _wrap(val)
            i += 1

    elif _is_object(value):
        for key in value:
            value[key] = _wrap(value[key])

    return value

def parse(value, *args, **kwargs):
    json = _json.loads(value, *args, **kwargs)
    wrapped = []
    for value in json:
        wrapped.append(_wrap(value))

    input = []
    for value in wrapped:
        if isinstance(value, _String):
            input.append(value.value)
        else:
            input.append(value)

    value = input[0]

    if _is_array(value):
        return _loop(_array_keys(value), input, [value], value)

    if _is_object(value):
        return _loop(_object_keys(value), input, [value], value)

    return value


def stringify(value, *args, **kwargs):
    known = _Known()
    input = []
    output = []
    i = int(_index(known, input, value))
    while i < len(input):
        output.append(_transform(known, input, input[i]))
        i += 1
    return _json.dumps(output, *args, **kwargs)

./resources\webview\test_webview\node_modules\shell-quote\print.py

#!/usr/bin/env python3
import sys
print(sys.argv[1])