pyside6增删改查插件

通过JSON文件类配置数据,生成CRUD的界面,支持列表和树结构,支持数据钻取。 


目录

一、form_config.json文件

二、同步版本

三、异步版本

三、效果图


一、form_config.json文件

{
  "id": 1,
  "title": "用户管理",
  "database": "sqlite:///CRUD_DEMO_DATABASE.db",
  "table": "user",
  "fields": [
    {
      "name": "id",
      "label": "编号",
      "type": "id",
      "placeholder": "0",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "name",
      "label": "名称",
      "type": "text",
      "placeholder": "张三丰",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "age",
      "label": "年龄",
      "type": "number",
      "placeholder": 99,
      "required": false,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "province",
      "label": "省份",
      "type": "select",
      "options": [
        {
          "label": "请选择...",
          "value": null
        },
        {
          "label": "浙江省",
          "value": "1"
        },
        {
          "label": "安徽省",
          "value": "2"
        },
        {
          "label": "广东省",
          "value": "3"
        },
        {
          "label": "江苏省",
          "value": "4"
        },
        {
          "label": "四川省",
          "value": "5"
        }
      ],
      "placeholder": "男",
      "required": false,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "create_time",
      "label": "创建时间",
      "type": "datetime",
      "placeholder": "2024-07-01 08:56:51",
      "required": false,
      "hidden": false,
      "is_search": true
    }
  ],
  "drill": [
    {
      "button_name": "我的菜单",
      "form_config_json": "./crud_form_config_tree.json",
      "foreign_key": "user_id"
    },
    {
      "button_name": "我的书籍",
      "form_config_json": "./crud_form_config_drill.json",
      "foreign_key": "user_id"
    }
  ]
}
{
  "id": 2,
  "title": "书籍管理",
  "database": "sqlite:///CRUD_DEMO_DATABASE.db",
  "table": "book",
  "fields": [
    {
      "name": "id",
      "label": "编号",
      "type": "id",
      "placeholder": "0",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "user_id",
      "label": "用户ID",
      "type": "number",
      "placeholder": "0",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "name",
      "label": "书名",
      "type": "text",
      "placeholder": "万历十五年",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "author",
      "label": "作者",
      "type": "text",
      "placeholder": "当年明月",
      "required": false,
      "hidden": false,
      "is_search": true
    }
  ],
  "drill": [
     {
      "button_name": "用户列表",
      "form_config_json": "./crud_form_config.json",
      "foreign_key": "user_id"
    }
  ]
}

{
  "id": 3,
  "title": "菜单管理",
  "database": "sqlite:///TREE_DEMO_DATABASE.db",
  "table": "menu",
  "fields": [
    {
      "name": "id",
      "label": "编号",
      "type": "id",
      "placeholder": "0",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "user_id",
      "label": "用户ID",
      "type": "number",
      "placeholder": "0",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "name",
      "label": "名称",
      "type": "text",
      "placeholder": "张三丰",
      "required": true,
      "hidden": false,
      "is_search": true
    },
    {
      "name": "parent_id",
      "label": "父节点",
      "type": "select",
      "options": [],
      "placeholder": "请选择父节点",
      "required": false,
      "hidden": false,
      "is_search": true
    }
  ],
  "drill": [
    {
      "button_name": "我的书籍",
      "form_config_json": "./crud_form_config_drill.json",
      "foreign_key": "user_id"
    }
  ]
}

二、同步版本

import json
import uuid

import dataset
import pandas as pd
from PySide6.QtGui import QRegularExpressionValidator
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QPushButton,
    QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QLabel, QSpinBox, QDialog, QDialogButtonBox, QDateTimeEdit,
    QGroupBox, QMessageBox, QTreeWidget, QTreeWidgetItem
)
from PySide6.QtCore import QDateTime, QThread, Signal

from plugIn.title_plugin import QCustomTitleBar, WindowResizer


class ExportThread(QThread):
    finished = Signal()

    def __init__(self, table):
        super().__init__()
        self.table = table

    def run(self):
        data = list(self.table.all())
        df = pd.DataFrame(data)
        df.to_excel('exported_data.xlsx', index=False)
        # self.finished.emit()
        pass


class CrudWidget(QWidget):
    def __init__(self, form_json: str = "./form_config.json", foreign_key=None, foreign_value=None):
        """
        :param form_json 表单文件
        :param foreign_key 外键名称
        :param foreign_value 外键值
        """
        super().__init__()
        self.foreign_key = foreign_key
        self.foreign_value = foreign_value

        # 初始化解析表单配置
        try:
            with open(form_json, 'r', encoding='utf-8') as f:
                self.config = json.load(f)  # 加载表单配置
        except FileNotFoundError:
            raise ValueError(f"表单配置文件未找到:{self.config}")
        if 'id' not in self.config:
            raise ValueError("表单配置文件格式错误【无id参数】")

        if 'fields' not in self.config:
            raise ValueError("表单配置文件格式错误【无fields参数】")

        if 'title' not in self.config:
            raise ValueError("表单配置文件格式错误【无title参数】")

        if 'database' not in self.config:
            raise ValueError("表单配置文件格式错误【无database参数】")

        if 'table' not in self.config:
            raise ValueError("表单配置文件格式错误【无table参数】")

        fields = self.config['fields']
        if type(fields) is not list:
            raise ValueError("表单配置文件格式错误【fields必须是list类型】")
        for field in fields:
            if 'name' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少name】")
            if 'label' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少label】")
            if 'type' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少type】")

        self.id = self.config['id']
        self.title = self.config['title']
        self.database = self.config['database']
        self.table = self.config['table']
        self.fields = self.config['fields']
        self.drill = self.config['drill']
        self.setWindowTitle(self.title)  # 设置窗口标题
        self.setGeometry(100, 100, 800, 600)  # 设置窗口位置和大小

        # self.title_bar = QCustomTitleBar(self, windowTitle=self.title)
        # self.resizer = WindowResizer(self)
        self.setStyleSheet("""
                    QLineEdit, QSpinBox {
                        padding: 5px;
                        border: 1px solid #d0d0d0;
                        border-radius: 5px;
                    }

                    QLabel {
                        font-size: 14px;
                        color: #333333;
                    }
                """)

        self.db = dataset.connect(self.database)  # 连接数据库
        self.table = self.db[self.config['table']]  # 获取数据表
        self.init_ui()  # 初始化UI
        self.load_data()  # 加载数据

    def init_ui(self):
        self.layout = QVBoxLayout(self)  # 创建垂直布局

        self.init_search_block()  # 初始化搜索块
        self.init_data_block()  # 初始化数据块

    def init_search_block(self):
        """初始化搜索块"""
        if hasattr(self, 'search_group') and hasattr(self, 'search_group_layout'):
            while self.search_group_layout.count():
                item = self.search_group_layout.takeAt(0)
                widget = item.widget()
                if widget is not None:
                    widget.deleteLater()
        else:

            self.search_group = QGroupBox()  # 创建搜索区组
            self.layout.addWidget(self.search_group)  # 将搜索布局添加到主布局
            self.search_group_layout = QHBoxLayout()  # 创建水平布局
            self.search_group.setLayout(self.search_group_layout)  # 设置组布局
        self.search_widgets = {}  # 搜索部件字典

        for field in self.config['fields']:
            if field['is_search']:  # 是否为搜索字段进行判断
                label = field['label']  # 获取字段标签
                widget = None
                if field['type'] == 'text':
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'number':
                    widget = QLineEdit()  # 创建数字输入框
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'datetime':
                    widget = QDateTimeEdit()
                    widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                    widget.setCalendarPopup(True)  # 启用弹出式日历选择
                elif field['type'] == 'select':
                    widget = QComboBox()
                    widget.currentIndexChanged.connect(self.search_data)  # 监听切换selection事件
                    for option in field['options']:
                        widget.addItem(option['label'], option['value'])
                elif field['type'] == 'switch':
                    # 使用QRadioButton
                    pass
                else:
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键

                self.search_widgets[field['name']] = widget  # 将输入框添加到字典
                self.search_group_layout.addWidget(QLabel(label))  # 添加标签到布局
                self.search_group_layout.addWidget(widget)  # 添加输入框到布局

        self.search_button = QPushButton("搜索")  # 创建搜索按钮
        self.search_button.clicked.connect(self.search_data)  # 连接搜索按钮信号
        self.reset_button = QPushButton("重置")  # 创建重置按钮
        self.reset_button.clicked.connect(self.reset_search)  # 连接重置按钮信号

        self.search_group_layout.addWidget(self.search_button)  # 添加搜索按钮到布局
        self.search_group_layout.addWidget(self.reset_button)  # 添加重置按钮到布局

    def init_data_block(self):
        self.data_layout = QVBoxLayout()  # 创建垂直布局

        control_group = QGroupBox()  # 创建控制区组
        control_group_layout = QVBoxLayout()  # 创建垂直布局
        control_group.setLayout(control_group_layout)  # 设置组布局

        self.control_layout = QHBoxLayout()  # 创建水平布局
        self.add_button = QPushButton("新增")  # 创建新增按钮
        self.add_button.clicked.connect(self.add_data)  # 连接新增按钮信号
        self.delete_button = QPushButton("删除")  # 创建删除按钮
        self.delete_button.clicked.connect(self.delete_data)  # 连接删除按钮信号
        self.clear_button = QPushButton("清空")  # 创建清空按钮
        self.clear_button.clicked.connect(self.clear_data)  # 连接清空按钮信号
        self.export_button = QPushButton("导出")  # 创建导出按钮
        self.export_button.clicked.connect(self.export_data)  # 连接导出按钮信号
        self.hide_search_button = QPushButton("隐藏搜索块")  # 创建隐藏搜索块按钮
        self.hide_search_button.clicked.connect(self.toggle_search_block)  # 连接隐藏搜索块按钮信号
        self.refresh_button = QPushButton("刷新数据")  # 创建刷新数据按钮
        self.refresh_button.clicked.connect(self.load_data)  # 连接刷新数据按钮信号

        self.control_layout.addWidget(self.add_button)  # 添加新增按钮到布局
        self.control_layout.addWidget(self.delete_button)  # 添加删除按钮到布局
        self.control_layout.addWidget(self.clear_button)  # 添加清空按钮到布局
        self.control_layout.addWidget(self.export_button)  # 添加导出按钮到布局
        self.control_layout.addStretch()  # 添加伸缩项
        self.control_layout.addWidget(self.hide_search_button)  # 添加隐藏搜索块按钮到布局
        self.control_layout.addWidget(self.refresh_button)  # 添加刷新数据按钮到布局

        control_group_layout.addLayout(self.control_layout)  # 将控制布局添加到数据布局

        self.data_layout.addWidget(control_group)  # 将控制布局添加到数据布局

        self.table_widget = QTableWidget()  # 创建表格部件
        self.table_widget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)  # 设置表格不可编辑
        # "详情", "编辑", "删除"为基础按钮,外加钻取按钮
        self.table_widget.setColumnCount(len(self.config['fields']) + 3 + len(self.drill))  # 设置列数
        headers = [field['label'] for field in self.config['fields']] + ["详情", "编辑", "删除"] + [d['button_name'] for
                                                                                                    d in
                                                                                                    self.drill]  # 设置表头
        self.table_widget.setHorizontalHeaderLabels(headers)  # 设置表头标签
        self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)  # 设置列宽自动调整
        self.table_widget.setSortingEnabled(True)  # 启用排序

        self.data_layout.addWidget(self.table_widget)  # 将表格部件添加到数据布局

        pagination_group = QGroupBox()  # 创建页码区组
        pagination_group_layout = QVBoxLayout()  # 创建垂直布局
        pagination_group.setLayout(pagination_group_layout)  # 设置组布局

        self.pagination_layout = QHBoxLayout()  # 创建水平布局
        self.current_page_label = QLabel("当前页码: 1")  # 创建当前页码标签
        self.total_records_label = QLabel("总记录数: 0")  # 创建总记录数标签
        self.page_size_combo = QComboBox()  # 创建每页显示条数下拉框
        self.page_size_combo.addItems(["5", "10", "20", "50", "100"])  # 添加选项
        self.page_size_combo.currentIndexChanged.connect(self.load_data)  # 连接下拉框信号
        self.prev_button = QPushButton("上一页")  # 创建上一页按钮
        self.prev_button.clicked.connect(self.prev_page)  # 连接上一页按钮信号
        self.next_button = QPushButton("下一页")  # 创建下一页按钮
        self.next_button.clicked.connect(self.next_page)  # 连接下一页按钮信号
        self.page_input = QLineEdit("1")  # 创建页码输入框
        self.page_input.setValidator(QRegularExpressionValidator(r'^(?:[0-9]|[1-9][0-9]|100)$'))  # 限制数字范围为 0 到 100
        self.page_input.textChanged.connect(
            lambda text: self.page_input.setText("1") if text == "" else None)  # 置空时自动设置为 1
        self.page_input.setFixedWidth(30)  # 设置输入框宽度
        self.jump_button = QPushButton("跳转")  # 创建跳转按钮
        self.jump_button.clicked.connect(self.jump_to_page)  # 连接跳转按钮信号

        self.pagination_layout.addWidget(self.current_page_label)  # 添加当前页码标签到布局
        self.pagination_layout.addWidget(self.total_records_label)  # 添加总记录数标签到布局
        self.pagination_layout.addWidget(QLabel("每页显示:"))  # 添加标签到布局
        self.pagination_layout.addWidget(self.page_size_combo)  # 添加下拉框到布局
        self.pagination_layout.addStretch()  # 添加伸缩项
        self.pagination_layout.addWidget(self.prev_button)  # 添加上一页按钮到布局
        self.pagination_layout.addWidget(self.next_button)  # 添加下一页按钮到布局
        self.pagination_layout.addWidget(self.page_input)  # 添加页码输入框到布局
        self.pagination_layout.addWidget(self.jump_button)  # 添加跳转按钮到布局

        pagination_group_layout.addLayout(self.pagination_layout)  # 将分页布局添加到数据布局

        self.data_layout.addWidget(pagination_group)  # 将分页布局添加到数据布局

        self.layout.addLayout(self.data_layout)  # 将数据布局添加到主布局

    def load_data(self):
        self.table_widget.setRowCount(0)  # 清空表格
        page_size = int(self.page_size_combo.currentText())  # 获取每页显示条数
        page = int(self.page_input.text())  # 获取当前页码
        offset = (page - 1) * page_size  # 计算偏移量

        conditions = {}  # 搜索条件字典
        if self.foreign_key and self.foreign_value:
            conditions[self.foreign_key] = self.foreign_value
        query = self.table.find(**conditions)  # 查询数据
        total_records = len(list(query))  # 获取总记录数
        self.total_records_label.setText(f"总记录数: {total_records}")  # 更新总记录数标签

        data = self.table.find(_limit=page_size, _offset=offset, **conditions)  # 分页查询数据
        for row_data in data:
            row = self.table_widget.rowCount()  # 获取当前行数
            self.table_widget.insertRow(row)  # 插入新行
            for i, field in enumerate(self.config['fields']):
                if field['name'] in row_data:
                    if field['type'] == 'select':
                        mapping = {}
                        for options in field['options']:
                            mapping[options['value']] = options['label']
                        item = QTableWidgetItem(str(mapping[row_data[field['name']]]))  # 创建表格项
                        self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
                    else:
                        item = QTableWidgetItem(str(row_data[field['name']]))  # 创建表格项
                        self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
                else:
                    item = QTableWidgetItem("")  # 创建表格项
                    self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
            detail_button = QPushButton("详情")  # 创建详情按钮
            detail_button.clicked.connect(lambda _, r=row: self.show_detail(r))  # 连接详情按钮信号
            self.table_widget.setCellWidget(row, len(self.config['fields']), detail_button)  # 设置详情按钮到单元格
            edit_button = QPushButton("编辑")  # 创建编辑按钮
            edit_button.clicked.connect(lambda _, r=row: self.edit_data(r))  # 连接编辑按钮信号
            self.table_widget.setCellWidget(row, len(self.config['fields']) + 1, edit_button)  # 设置编辑按钮到单元格
            delete_button = QPushButton("删除")  # 创建删除按钮
            delete_button.clicked.connect(lambda _, r=row: self.delete_data_row(r))  # 连接删除按钮信号
            self.table_widget.setCellWidget(row, len(self.config['fields']) + 2, delete_button)  # 设置删除按钮到单元格
            # 数据钻取按钮
            for index, d in enumerate(self.drill):
                drill_button = QPushButton(d['button_name'])  # 创建钻取按钮
                drill_button.clicked.connect(lambda checked, r=row, d=d: self.drill_data_row(r, d))  # 连接删除按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']) + 2 + index + 1,
                                                drill_button)  # 设置删除按钮到单元格

        self.current_page_label.setText(f"当前页码: {page}")  # 更新当前页码标签

    def search_data(self):
        if not hasattr(self, 'table_widget'):
            return  # 如果没有表格,则不执行操作
        conditions = {}  # 搜索条件字典
        if self.foreign_key and self.foreign_value:
            conditions[self.foreign_key] = self.foreign_value
        for name, widget in self.search_widgets.items():
            if isinstance(widget, QComboBox):
                # value = widget.currentText()  # 获取下拉框选中项
                value = widget.currentData()

            elif isinstance(widget, QDateTimeEdit):
                value = widget.text()
                if widget.text() == '2000-01-01 00:00:00':
                    value = None
            else:
                value = widget.text()  # 获取输入框文本
            if value:
                conditions[name] = value  # 添加到搜索条件字典
        print(conditions)
        query = self.table.find(**conditions)  # 根据条件查询数据
        data = list(query)  # 获取查询结果

        self.table_widget.setRowCount(0)  # 清空表格
        for row_data in data:
            row = self.table_widget.rowCount()  # 获取当前行数
            self.table_widget.insertRow(row)  # 插入新行
            for i, field in enumerate(self.config['fields']):
                if field['type'] == 'select':
                    mapping = {}
                    for options in field['options']:
                        mapping[options['value']] = options['label']
                    item = QTableWidgetItem(str(mapping[row_data[field['name']]]))  # 创建表格项
                    self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
                else:
                    item = QTableWidgetItem(str(row_data[field['name']]))  # 创建表格项
                    self.table_widget.setItem(row, i, item)  # 设置表格项到单元格

            detail_button = QPushButton("详情")  # 创建详情按钮
            detail_button.clicked.connect(lambda _, r=row: self.show_detail(r))  # 连接详情按钮信号
            self.table_widget.setCellWidget(row, len(self.config['fields']), detail_button)  # 设置详情按钮到单元格
            edit_button = QPushButton("编辑")  # 创建编辑按钮
            edit_button.clicked.connect(lambda _, r=row: self.edit_data(r))  # 连接编辑按钮信号
            self.table_widget.setCellWidget(row, len(self.config['fields']) + 1, edit_button)  # 设置编辑按钮到单元格
            delete_button = QPushButton("删除")  # 创建删除按钮
            delete_button.clicked.connect(lambda _, r=row: self.delete_data_row(r))  # 连接删除按钮信号
            self.table_widget.setCellWidget(row, len(self.config['fields']) + 2, delete_button)  # 设置删除按钮到单元格
            # 数据钻取按钮
            for index, d in enumerate(self.drill):
                drill_button = QPushButton(d['button_name'])  # 创建钻取按钮
                drill_button.clicked.connect(lambda checked, r=row, d=d: self.drill_data_row(r, d))  # 连接删除按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']) + 2 + index + 1,
                                                drill_button)  # 设置删除按钮到单元格

    def reset_search(self):
        self.init_search_block()
        self.load_data()  # 重新加载数据

    def add_data(self):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("新增数据")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        form_widgets = {}  # 表单部件字典
        for field in self.config['fields']:
            label = field['label']  # 获取字段标签
            if field['type'] == 'id':
                pass
            elif field['type'] == 'text':
                widget = QLineEdit()  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'number':
                widget = QLineEdit()  # 创建数字输入框
                validator = QRegularExpressionValidator(r'^\d*$')
                widget.setValidator(validator)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'datetime':
                widget = QDateTimeEdit()
                widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                widget.setCalendarPopup(True)  # 启用弹出式日历选择
                widget.setDateTime(QDateTime.currentDateTime())
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setDateTime(self.foreign_value)
            elif field['type'] == 'select':
                widget = QComboBox()
                for option in field['options']:
                    widget.addItem(option['label'], option['value'])
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setEditable(False)
                    widget.setCurrentIndex(self.foreign_value)
            else:
                widget = QLineEdit()  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        if dialog.exec() == QDialog.DialogCode.Accepted:
            data = {}
            for name, widget in form_widgets.items():
                if isinstance(widget, QLineEdit):
                    data[name] = widget.text()
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                elif isinstance(widget, QSpinBox):
                    data[name] = widget.value()
                elif isinstance(widget, QDateTimeEdit):
                    data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                elif isinstance(widget, QComboBox):
                    data[name] = widget.currentData()
            self.table.insert(data)  # 插入数据
            self.load_data()  # 重新加载数据

    def delete_data(self):
        # 删除数据逻辑
        selected_rows = set()
        for item in self.table_widget.selectedItems():
            selected_rows.add(item.row())

        if not selected_rows:
            QMessageBox.warning(self, "警告", "请选择要删除的行")
            return

        confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            for row in sorted(selected_rows, reverse=True):
                # 获取要删除的记录的ID
                id_item = self.table_widget.item(row, 0)  # 假设ID在第一列
                if id_item is not None:
                    record_id = int(id_item.text())
                    # 从数据库中删除记录
                    self.table.delete(id=record_id)
                    # 从表格中删除行
                    self.table_widget.removeRow(row)

    def clear_data(self):
        confirm = QMessageBox.warning(self, "确认清空", "你确定要清空所有数据吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            self.table.delete()  # 清空数据表
            self.load_data()  # 重新加载数据

    def export_data(self):
        self.export_thread = ExportThread(self.table)
        self.export_thread.finished.connect(self.export_finished)
        self.export_thread.start()

    def export_finished(self):
        QMessageBox.information(self, "导出完成", "数据已成功导出到 exported_data.xlsx")

    def toggle_search_block(self):
        if self.search_group.isVisible():
            self.search_group.setVisible(False)
            self.hide_search_button.setText("显示搜索块")
        else:
            self.search_group.setVisible(True)
            self.hide_search_button.setText("隐藏搜索块")

    def prev_page(self):
        current_page = int(self.page_input.text())  # 获取当前页码
        if current_page > 1:
            self.page_input.setText(str(current_page - 1))  # 更新页码
            self.load_data()  # 重新加载数据

    def next_page(self):
        current_page = int(self.page_input.text())  # 获取当前页码
        page_size = int(self.page_size_combo.currentText())  # 获取每页显示条数
        total_records = int(self.total_records_label.text().split(": ")[1])  # 获取总记录数
        if (current_page * page_size) < total_records:
            self.page_input.setText(str(current_page + 1))  # 更新页码
            self.load_data()  # 重新加载数据

    def jump_to_page(self):
        self.load_data()  # 重新加载数据

    def show_detail(self, row):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("数据详情")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        for i, field in enumerate(self.config['fields']):
            label = field['label']  # 获取字段标签
            value = self.table_widget.item(row, i).text()  # 获取表格项文本
            form_layout.addRow(label, QLabel(value))  # 添加标签和值到布局

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        dialog.exec()  # 显示对话框

    def edit_data(self, row):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("编辑数据")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        form_widgets = {}  # 表单部件字典
        for i, field in enumerate(self.config['fields']):
            label = field['label']  # 获取字段标签
            value = self.table_widget.item(row, i).text()  # 获取表格项文本
            if field['type'] == 'id':
                widget = QLineEdit(value)  # 创建标签框,ID不可编辑
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                widget.setReadOnly(True)
            elif field['type'] == 'text':
                widget = QLineEdit(value)  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'number':
                widget = QLineEdit(value)  # 创建数字输入框
                validator = QRegularExpressionValidator(r'^\d*$')
                widget.setValidator(validator)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'datetime':
                widget = QDateTimeEdit()
                widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                widget.setCalendarPopup(True)  # 启用弹出式日历选择
                widget.setDateTime(QDateTime.fromString(value, "yyyy-MM-dd hh:mm:ss"))
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setDateTime(self.foreign_value)
            elif field['type'] == 'select':
                widget = QComboBox()
                for option in field['options']:
                    widget.addItem(option['label'], option['value'])
                index = widget.findText(value)
                widget.setCurrentIndex(index)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setEditable(False)
                    widget.setCurrentIndex(self.foreign_value)
            else:
                widget = QLineEdit(value)  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        if dialog.exec() == QDialog.DialogCode.Accepted:
            data = {}
            for name, widget in form_widgets.items():
                if isinstance(widget, QLineEdit) or isinstance(widget, QLabel):
                    data[name] = widget.text()
                elif isinstance(widget, QSpinBox):
                    data[name] = widget.value()
                elif isinstance(widget, QDateTimeEdit):
                    data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                elif isinstance(widget, QComboBox):
                    data[name] = widget.currentData()
            self.table.update(data, ['id'])  # 更新数据
            self.load_data()  # 重新加载数据

    def delete_data_row(self, row):
        confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            # 获取要删除的记录的ID
            id_item = self.table_widget.item(row, 0)  # 假设ID在第一列
            if id_item is not None:
                record_id = int(id_item.text())
                # 从数据库中删除记录
                self.table.delete(id=record_id)
                # 从表格中删除行
                self.table_widget.removeRow(row)

    def drill_data_row(self, row: int, drill: dict):
        # 获取记录的ID
        id_item = self.table_widget.item(row, 0)  # 假设ID在第一列
        if id_item is not None:
            record_id = int(id_item.text())  # 数据ID
            button_name = drill['button_name']
            drill_form_config_json_path = drill['form_config_json']
            foreign_key = drill['foreign_key']
            drill_form_config_json_data = json.load(open(drill_form_config_json_path, 'r', encoding='utf-8'))
            window_id = f"crud_widget_{drill_form_config_json_data['id']}"

            # 防止多次点击打开多个窗口
            if not hasattr(self, window_id) or getattr(self, window_id) is None:
                if 'parent_id' in [f['name'] for f in drill_form_config_json_data['fields']]:
                    widget: TreeWidget = TreeWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
                                                    foreign_value=record_id)
                else:
                    pass
                    widget: CrudWidget = CrudWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
                                                    foreign_value=record_id)
                    widget.isVisible()
                    widget.isEnabled()
                setattr(self, window_id, widget)
                widget.show()
            else:
                # 每次重现的时候将参数强制赋值进去
                widget: CrudWidget = getattr(self, window_id)
                setattr(widget, "form_json", drill_form_config_json_path)
                setattr(widget, "foreign_key", foreign_key)
                setattr(widget, "foreign_value", record_id)
                widget.show()
                widget.activateWindow()  # 激活窗口
                widget.raise_()  # 将窗口提升到最前面

    def showEvent(self, event):
        """
        这个函数必须重写一下。子窗口关闭后,引用还在上一层存着,并不会销毁,而是隐藏。再次显示并不会初始化。所以在显示的时候我们手动初始化一下。
        """
        super().showEvent(event)
        self.load_data()  # 加载数据


class TreeWidget(QWidget):
    def __init__(self, form_json: str | dict = "./tree_form_config.json", foreign_key=None, foreign_value=None):
        """
        :param form_json 表单文件
        :param foreign_key 外键名称
        :param foreign_value 外键值
        """
        super().__init__()
        self.foreign_key = foreign_key
        self.foreign_value = foreign_value

        # 初始化解析表单配置
        try:
            with open(form_json, 'r', encoding='utf-8') as f:
                self.config = json.load(f)  # 加载表单配置
        except FileNotFoundError:
            raise ValueError(f"表单配置文件未找到:{self.config}")
        if 'id' not in self.config:
            raise ValueError("表单配置文件格式错误【无id参数】")

        if 'fields' not in self.config:
            raise ValueError("表单配置文件格式错误【无fields参数】")

        if 'title' not in self.config:
            raise ValueError("表单配置文件格式错误【无title参数】")

        if 'database' not in self.config:
            raise ValueError("表单配置文件格式错误【无database参数】")

        if 'table' not in self.config:
            raise ValueError("表单配置文件格式错误【无table参数】")

        fields = self.config['fields']
        if type(fields) is not list:
            raise ValueError("表单配置文件格式错误【fields必须是list类型】")
        for field in fields:
            if 'name' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少name】")
            if 'label' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少label】")
            if 'type' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少type】")
        if 'parent_id' not in [f['name'] for f in fields]:
            raise ValueError("表单配置文件格式错误【fields缺少parent_id】")

        self.id = self.config['id']
        self.title = self.config['title']
        self.database = self.config['database']
        self.table = self.config['table']
        self.fields = self.config['fields']
        self.drill = self.config['drill']
        self.setWindowTitle(self.title)  # 设置窗口标题
        self.setGeometry(100, 100, 800, 600)  # 设置窗口位置和大小

        # self.title_bar = QCustomTitleBar(self, windowTitle=self.title)
        # self.resizer = WindowResizer(self)

        self.setStyleSheet("""
                            QLineEdit, QSpinBox {
                                padding: 5px;
                                border: 1px solid #d0d0d0;
                                border-radius: 5px;
                            }

                            QLabel {
                                font-size: 14px;
                                color: #333333;
                            }
                        """)

        self.db = dataset.connect(self.database)  # 连接数据库
        self.table = self.db[self.config['table']]  # 获取数据表
        self.init_ui()  # 初始化UI
        self.load_data()  # 加载数据

    def init_ui(self):
        self.layout = QVBoxLayout(self)  # 创建垂直布局

        self.init_search_block()  # 初始化搜索块
        self.init_data_block()  # 初始化数据块

    def init_search_block(self):
        """初始化搜索块"""
        if hasattr(self, 'search_group') and hasattr(self, 'search_group_layout'):
            while self.search_group_layout.count():
                item = self.search_group_layout.takeAt(0)
                widget = item.widget()
                if widget is not None:
                    widget.deleteLater()
        else:

            self.search_group = QGroupBox()  # 创建搜索区组
            self.layout.addWidget(self.search_group)  # 将搜索布局添加到主布局
            self.search_group_layout = QHBoxLayout()  # 创建水平布局
            self.search_group.setLayout(self.search_group_layout)  # 设置组布局
        self.search_widgets = {}  # 搜索部件字典

        for field in self.config['fields']:
            if field['is_search']:  # 是否为搜索字段进行判断
                label = field['label']  # 获取字段标签
                widget = None
                if field['type'] == 'text':
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'number':
                    widget = QLineEdit()  # 创建数字输入框
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'datetime':
                    widget = QDateTimeEdit()
                    widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                    widget.setCalendarPopup(True)  # 启用弹出式日历选择
                elif field['type'] == 'select':
                    widget = QComboBox()
                    widget.currentIndexChanged.connect(self.search_data)  # 监听切换selection事件
                    self.update_parent_options(widget)
                else:
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键

                self.search_widgets[field['name']] = widget  # 将输入框添加到字典
                self.search_group_layout.addWidget(QLabel(label))  # 添加标签到布局
                self.search_group_layout.addWidget(widget)  # 添加输入框到布局

        self.search_button = QPushButton("搜索")  # 创建搜索按钮
        self.search_button.clicked.connect(self.search_data)  # 连接搜索按钮信号
        self.reset_button = QPushButton("重置")  # 创建重置按钮
        self.reset_button.clicked.connect(self.reset_search)  # 连接重置按钮信号

        self.search_group_layout.addWidget(self.search_button)  # 添加搜索按钮到布局
        self.search_group_layout.addWidget(self.reset_button)  # 添加重置按钮到布局

    def init_data_block(self):
        self.data_layout = QVBoxLayout()  # 创建垂直布局

        control_group = QGroupBox()  # 创建控制区组
        control_group_layout = QVBoxLayout()  # 创建垂直布局
        control_group.setLayout(control_group_layout)  # 设置组布局

        self.control_layout = QHBoxLayout()  # 创建水平布局
        self.add_button = QPushButton("新增")  # 创建新增按钮
        self.add_button.clicked.connect(self.add_data)  # 连接新增按钮信号
        self.delete_button = QPushButton("删除")  # 创建删除按钮
        self.delete_button.clicked.connect(self.delete_data)  # 连接删除按钮信号
        self.clear_button = QPushButton("清空")  # 创建清空按钮
        self.clear_button.clicked.connect(self.clear_data)  # 连接清空按钮信号
        self.refresh_button = QPushButton("刷新数据")  # 创建刷新数据按钮
        self.refresh_button.clicked.connect(self.load_data)  # 连接刷新数据按钮信号
        # 初始状态为收起
        self.is_expanded = False
        self.expand_button = QPushButton("一键展开")  # 创建刷新数据按钮
        self.expand_button.clicked.connect(self.toggle_expand_collapse)  # 连接刷新数据按钮信号

        self.control_layout.addWidget(self.add_button)  # 添加新增按钮到布局
        self.control_layout.addWidget(self.delete_button)  # 添加删除按钮到布局
        self.control_layout.addWidget(self.clear_button)  # 添加清空按钮到布局
        self.control_layout.addStretch()  # 添加伸缩项
        self.control_layout.addWidget(self.expand_button)  # 添加展开数据按钮到布局
        self.control_layout.addWidget(self.refresh_button)  # 添加刷新数据按钮到布局

        control_group_layout.addLayout(self.control_layout)  # 将控制布局添加到数据布局

        self.data_layout.addWidget(control_group)  # 将控制布局添加到数据布局

        self.tree_widget = QTreeWidget()  # 创建树形部件
        # "详情", "编辑", "删除"为基础按钮,外加钻取按钮
        self.tree_widget.setColumnCount(len(self.config['fields']) + 3 + len(self.drill))  # 设置列数
        headers = [field['label'] for field in self.config['fields']] + ["详情", "编辑", "删除"] + [d['button_name'] for
                                                                                                    d in
                                                                                                    self.drill]  # 设置表头
        self.tree_widget.setHeaderLabels(headers)  # 设置表头
        self.tree_widget.header().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)  # 设置列宽自动调整
        self.tree_widget.setSortingEnabled(True)  # 启用排序

        self.data_layout.addWidget(self.tree_widget)  # 将树形部件添加到数据布局

        self.layout.addLayout(self.data_layout)  # 将数据布局添加到主布局

    def load_data(self):
        self.tree_widget.clear()  # 清空树形部件
        conditions = {}  # 搜索条件字典
        if self.foreign_key and self.foreign_value:
            conditions[self.foreign_key] = self.foreign_value
        data = self.table.all(**conditions)  # 查询数据
        self.build_tree(list(data))  # 构建树形结构
        self.tree_widget.expandAll()  # 展开树结构

    def build_tree(self, data):
        nodes = {}

        for row_data in data:
            item = []
            for field in self.config['fields']:
                if field['name'] in row_data:
                    item.append(str(row_data[field['name']]))
                else:
                    item.append(None)
            node = QTreeWidgetItem(item)
            nodes[row_data['id']] = node
        for row_data in data:
            if row_data['parent_id'] is None:
                self.tree_widget.addTopLevelItem(nodes[row_data['id']])
            else:
                parent_node = nodes.get(row_data['parent_id'])
                if parent_node is None:
                    parent_node = nodes.get(int(row_data['parent_id']))
                if parent_node is not None:
                    parent_node.addChild(nodes[row_data['id']])
                else:
                    self.tree_widget.addTopLevelItem(nodes[row_data['id']])
            self.add_buttons(nodes[row_data['id']])

    def add_buttons(self, node):
        detail_button = QPushButton("详情")  # 创建详情按钮
        detail_button.clicked.connect(lambda _, n=node: self.show_detail(n))  # 连接详情按钮信号
        self.tree_widget.setItemWidget(node, len(self.config['fields']), detail_button)  # 设置详情按钮到单元格
        edit_button = QPushButton("编辑")  # 创建编辑按钮
        edit_button.clicked.connect(lambda _, n=node: self.edit_data(n))  # 连接编辑按钮信号
        self.tree_widget.setItemWidget(node, len(self.config['fields']) + 1, edit_button)  # 设置编辑按钮到单元格
        delete_button = QPushButton("删除")  # 创建删除按钮
        delete_button.clicked.connect(lambda _, n=node: self.delete_data_node(n))  # 连接删除按钮信号
        self.tree_widget.setItemWidget(node, len(self.config['fields']) + 2, delete_button)  # 设置删除按钮到单元格
        # 数据钻取按钮
        for index, d in enumerate(self.drill):
            drill_button = QPushButton(d['button_name'])  # 创建钻取按钮
            drill_button.clicked.connect(lambda _, n=node: self.drill_data_row(n, d))  # 连接删除按钮信号
            self.tree_widget.setItemWidget(node, len(self.config['fields']) + 2 + index + 1, drill_button)  # 设置删除按钮到单元格

    def search_data(self):
        conditions = {}  # 搜索条件字典
        if self.foreign_key and self.foreign_value:
            conditions[self.foreign_key] = self.foreign_value
        for name, widget in self.search_widgets.items():
            if isinstance(widget, QComboBox):
                value = widget.currentData()
            else:
                value = widget.text()  # 获取输入框文本
            if value:
                conditions[name] = value  # 添加到搜索条件字典

        if 'parent_id' in conditions and conditions['parent_id'] is not None:
            params = ""
            for index, (k, v) in enumerate(conditions.items()):
                params = params + f" {k}=:{k}"
                if index != len(conditions.items()) - 1:
                    params = params + " and"
            query = rf"""
                                WITH RECURSIVE descendants AS (
                                    SELECT id, name, parent_id
                                    FROM {self.config['table']}
                                    WHERE {params}
                                    UNION ALL
                                    SELECT cdt.id, cdt.name, cdt.parent_id
                                    FROM {self.config['table']} cdt
                                    INNER JOIN descendants d ON cdt.parent_id = d.id
                                )
                                SELECT * FROM descendants
                                """
            data = list(self.db.query(query, **conditions))
        else:
            query = self.table.find(**conditions)  # 根据条件查询数据
            data = list(query)  # 获取查询结果

        if hasattr(self, 'tree_widget'):
            self.tree_widget.clear()  # 清空树形部件
        else:
            self.tree_widget = QTreeWidget()  # 创建树形部件
        self.build_tree(data)  # 构建树形结构
        self.tree_widget.expandAll() # 展开树结构

    def reset_search(self):
        self.init_search_block()
        self.load_data()  # 重新加载数据

    def add_data(self):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("新增数据")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        form_widgets = {}  # 表单部件字典
        for field in self.config['fields']:
            label = field['label']  # 获取字段标签
            if field['type'] == 'id':
                pass
            elif field['type'] == 'text':
                widget = QLineEdit()  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'number':
                widget = QLineEdit()  # 创建数字输入框
                validator = QRegularExpressionValidator(r'^\d*$')
                widget.setValidator(validator)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'datetime':
                widget = QDateTimeEdit()
                widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                widget.setCalendarPopup(True)  # 启用弹出式日历选择
                widget.setDateTime(QDateTime.currentDateTime())
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setDateTime(self.foreign_value)
            elif field['type'] == 'select':
                widget = QComboBox()
                self.update_parent_options(widget)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setEditable(False)
                    widget.setCurrentIndex(self.foreign_value)
            else:
                widget = QLineEdit()  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        if dialog.exec() == QDialog.DialogCode.Accepted:
            data = {}
            for name, widget in form_widgets.items():
                if isinstance(widget, QLineEdit):
                    data[name] = widget.text()
                elif isinstance(widget, QSpinBox):
                    data[name] = widget.value()
                elif isinstance(widget, QDateTimeEdit):
                    data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                elif isinstance(widget, QComboBox):
                    data[name] = widget.currentData()
            self.table.insert(data)  # 插入数据
            self.load_data()  # 重新加载数据

    def delete_data(self):
        selected_items = self.tree_widget.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "警告", "请选择要删除的行")
            return

        confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            for item in selected_items:
                self.delete_item(item)

    def delete_item(self, item):
        id_item = item.text(0)  # 假设ID在第一列
        if id_item is not None:
            record_id = int(id_item)
            self.table.delete(id=record_id)
            parent = item.parent()
            if parent is None:
                self.tree_widget.takeTopLevelItem(self.tree_widget.indexOfTopLevelItem(item))
            else:
                parent.removeChild(item)

    def toggle_expand_collapse(self):
        if self.is_expanded:
            self.tree_widget.collapseAll()
            self.expand_button.setText("一键展开")
        else:
            self.tree_widget.expandAll()
            self.expand_button.setText("一键收起")
        self.is_expanded = not self.is_expanded

    def clear_data(self):
        confirm = QMessageBox.warning(self, "确认清空", "你确定要清空所有数据吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            self.table.delete()  # 清空数据表
            self.load_data()  # 重新加载数据

    def update_parent_options(self, combo_box, self_id=None):
        combo_box.clear()
        combo_box.addItem("请选择父节点", None)

        conditions = {}  # 搜索条件字典
        if self.foreign_key and self.foreign_value:
            conditions[self.foreign_key] = self.foreign_value
        data = self.table.find(**conditions)
        for row_data in data:
            # 有些时候父节点和子节点是同一个,所以需要排除这种情况
            if self_id is not None and str(row_data['id']) == str(self_id):
                continue

            combo_box.addItem(row_data['name'], row_data['id'])

    def show_detail(self, node):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("数据详情")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        for i, field in enumerate(self.config['fields']):
            label = field['label']  # 获取字段标签
            value = node.text(i)  # 获取表格项文本
            form_layout.addRow(label, QLabel(value))  # 添加标签和值到布局

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        dialog.exec()  # 显示对话框

    def edit_data(self, node):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("编辑数据")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        form_widgets = {}  # 表单部件字典
        for i, field in enumerate(self.config['fields']):
            label = field['label']  # 获取字段标签
            value = node.text(i)  # 获取节点文本
            if field['type'] == 'id':
                widget = QLineEdit(value)  # 创建标签框,ID不可编辑
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                widget.setReadOnly(True)
            elif field['type'] == 'text':
                widget = QLineEdit(value)  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'number':
                widget = QLineEdit(value)  # 创建数字输入框
                validator = QRegularExpressionValidator(r'^\d*$')
                widget.setValidator(validator)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'datetime':
                widget = QDateTimeEdit()
                widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                widget.setCalendarPopup(True)  # 启用弹出式日历选择
                widget.setDateTime(QDateTime.fromString(value, "yyyy-MM-dd hh:mm:ss"))
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setDateTime(self.foreign_value)
            elif field['type'] == 'select':
                widget = QComboBox()
                self.update_parent_options(widget, self_id=node.text(0))
                if value and value != 'None':
                    index = widget.findData(int(value))
                    widget.setCurrentIndex(index)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setEditable(False)
                    widget.setCurrentIndex(self.foreign_value)
            else:
                widget = QLineEdit(value)  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        if dialog.exec() == QDialog.DialogCode.Accepted:
            data = {}
            for name, widget in form_widgets.items():
                if isinstance(widget, QLineEdit) or isinstance(widget, QLabel):
                    data[name] = widget.text()
                elif isinstance(widget, QSpinBox):
                    data[name] = widget.value()
                elif isinstance(widget, QDateTimeEdit):
                    data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                elif isinstance(widget, QComboBox):
                    data[name] = widget.currentData()
            if str(data['parent_id']) == str(data['id']):
                raise Exception("父节点ID不能等于自身ID")
            # 还有一种情况要排除掉,那就是两个节点,互为其父节点。
            one = self.table.find_one(id=data['parent_id'])
            if one and one['parent_id'] == data['id']:
                raise Exception("父节点不能为其子节点")
            self.table.update(data, ['id'])  # 更新数据
            self.load_data()  # 重新加载数据

    def delete_data_node(self, node):
        confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            id_item = node.text(0)  # 假设ID在第一列
            if id_item is not None:
                record_id = int(id_item)
                self.table.delete(id=record_id)
                parent = node.parent()
                if parent is None:
                    self.tree_widget.takeTopLevelItem(self.tree_widget.indexOfTopLevelItem(node))
                else:
                    parent.removeChild(node)

    def drill_data_row(self, node, drill: dict):
        # 获取记录的ID
        record_id = node.text(0)  # 假设ID在第一列
        if record_id is not None:
            button_name = drill['button_name']
            drill_form_config_json_path = drill['form_config_json']
            foreign_key = drill['foreign_key']
            drill_form_config_json_data = json.load(open(drill_form_config_json_path, 'r', encoding='utf-8'))
            window_id = f"crud_widget_{drill_form_config_json_data['id']}"
            # 防止多次点击打开多个窗口
            if not hasattr(self, window_id) or getattr(self, window_id) is None:
                if 'parent_id' in [f['name'] for f in drill_form_config_json_data['fields']]:
                    widget: TreeWidget = TreeWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
                                                    foreign_value=record_id)
                else:
                    pass
                    widget: CrudWidget = CrudWidget(form_json=drill_form_config_json_path, foreign_key=foreign_key,
                                                    foreign_value=record_id)
                    widget.isVisible()
                    widget.isEnabled()
                setattr(self, window_id, widget)
                widget.show()
            else:
                # 每次重现的时候将参数强制赋值进去
                widget = getattr(self, window_id)
                setattr(widget, "form_json", drill_form_config_json_path)
                setattr(widget, "foreign_key", foreign_key)
                setattr(widget, "foreign_value", record_id)
                widget.show()
                widget.activateWindow()  # 激活窗口
                widget.raise_()  # 将窗口提升到最前面

    def showEvent(self, event):
        """
        这个函数必须重写一下。子窗口关闭后,引用还在上一层存着,并不会销毁,而是隐藏。再次显示并不会初始化。所以在显示的时候我们手动初始化一下。
        """
        super().showEvent(event)
        self.init_search_block()
        self.load_data()  # 加载数据


class ExistingWindow(QWidget):
    def __init__(self, parent=None):
        super(ExistingWindow, self).__init__(parent)
        self.setWindowTitle("Existing Window")
        self.resize(600, 400)
        self.title_bar = QCustomTitleBar(self, windowTitle="配置")
        self.resizer = WindowResizer(self)
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)
        # 其他初始化代码
        self.button = QPushButton("Test")
        self.layout.addWidget(self.button)

        self.crud_widget = None  # 用于存储CrudWidget实例

        def btn_event():
            # 防止多次点击打开多个窗口
            if not hasattr(self, "crud_widget") or self.crud_widget is None:
                self.crud_widget = CrudWidget(form_json='crud_form_config.json')
                self.crud_widget.show()
            else:
                self.crud_widget.show()
                self.crud_widget.activateWindow()  # 激活窗口
                self.crud_widget.raise_()  # 将窗口提升到最前面

        self.button.clicked.connect(btn_event)


if __name__ == '__main__':
    app = QApplication([])
    main = ExistingWindow()
    main.show()
    app.exec()

三、异步版本

import json
import os
import sys

import pandas as pd
from PySide6.QtGui import QRegularExpressionValidator
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QPushButton,
    QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QLabel, QSpinBox, QDialog, QDialogButtonBox, QDateTimeEdit,
    QGroupBox, QMessageBox, QTreeWidget, QTreeWidgetItem, QTextEdit
)
from PySide6.QtCore import QDateTime, QThread, Signal, Qt
from qasync import QEventLoop, asyncSlot

from plugIn.background_plugin import BackgroundWidget
from plugIn.jdbc.mysql_jdbc import *
from plugIn.widgets_enhance.file_picker import FilePicker
from plugIn.widgets_enhance.folder_picker import FolderPicker


class ExportThread(QThread):
    finished = Signal()

    def __init__(self, data):
        super().__init__()
        self.data = data

    def run(self):
        df = pd.DataFrame(self.data)
        df.to_excel('exported_data.xlsx', index=False)
        # self.finished.emit()
        pass


class CrudWidget(QWidget):
    def __init__(self, parent: QWidget | None, parent_data: dict, form_json: str = "./form_config.json",
                 foreign_key=None, foreign_value=None):
        """
        :param parent 上层对象
        :param parent_data 上层对象的数据,这是一个层层传递的数据,用来记录钻取过程中每个层级的数据。key为JSON文件的id,值为钻取数据的id
        :param form_json 表单文件
        :param foreign_key 外键名称
        :param foreign_value 外键值
        """
        super().__init__()
        self.parent = parent
        self.parent_data = parent_data
        self.form_json = form_json
        self.foreign_key = foreign_key
        self.foreign_value = foreign_value
        # 初始化解析表单配置
        try:
            with open(form_json, 'r', encoding='utf-8') as f:
                self.config = json.load(f)  # 加载表单配置
        except FileNotFoundError:
            raise ValueError(f"表单配置文件未找到:{self.config}")
        if 'id' not in self.config:
            raise ValueError("表单配置文件格式错误【无id参数】")

        if 'fields' not in self.config:
            raise ValueError("表单配置文件格式错误【无fields参数】")

        if 'title' not in self.config:
            raise ValueError("表单配置文件格式错误【无title参数】")

        if 'database' not in self.config:
            raise ValueError("表单配置文件格式错误【无database参数】")

        if 'table' not in self.config:
            raise ValueError("表单配置文件格式错误【无table参数】")

        fields = self.config['fields']
        if type(fields) is not list:
            raise ValueError("表单配置文件格式错误【fields必须是list类型】")
        for field in fields:
            if 'name' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少name】")
            if 'label' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少label】")
            if 'type' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少type】")

        self.id = self.config['id']
        self.title = self.config['title']
        self.database = self.config['database']
        self.background = self.config['background'] if 'background' in self.config else '#ffffff'
        self.geometry = self.config['geometry'] if 'geometry' in self.config else [100, 100, 1000, 600]
        self.fields = self.config['fields']
        # 字段名与类型的映射
        self.field_type_mapping = {field['name']: field['type'] for field in fields}
        # 排序字段
        order_fields = sorted([field for field in fields if "order_by" in field], key=lambda x: x["order_by"])
        self.order_by = [field['name'] for field in order_fields]
        self.drill = self.config['drill']
        self.setWindowTitle(self.title)  # 设置窗口标题
        self.setGeometry(*self.geometry)
        self.setStyleSheet("""
                    QLineEdit, QSpinBox {
                        padding: 5px;
                        border: 1px solid #d0d0d0;
                        border-radius: 5px;
                    }

                    QLabel {
                        font-size: 14px;
                        color: #333333;
                    }
                """)

        self.table_name = self.config['table']
        # self.adjustSize()#自动调整大小
        # 在这里引入背景插件并显示
        self.custom_main_window = BackgroundWidget(self, False, False)
        self.custom_main_window.show()
        asyncio.ensure_future(self.init_ui())

    async def init_ui(self):
        self.layout = QVBoxLayout(self)  # 创建垂直布局
        await self.init_search_block()  # 初始化搜索块
        await self.init_data_block()  # 初始化数据块
        await self.load_data()  # 加载数据

    async def init_search_block(self):
        """初始化搜索块"""
        if hasattr(self, 'search_group') and hasattr(self, 'search_group_layout'):
            while self.search_group_layout.count():
                item = self.search_group_layout.takeAt(0)
                widget = item.widget()
                if widget is not None:
                    widget.deleteLater()
        else:

            self.search_group = QGroupBox()  # 创建搜索区组
            self.layout.addWidget(self.search_group)  # 将搜索布局添加到主布局
            self.search_group_layout = QHBoxLayout()  # 创建水平布局
            self.search_group.setLayout(self.search_group_layout)  # 设置组布局
        self.search_widgets = {}  # 搜索部件字典

        for field in self.config['fields']:
            if field['is_search']:  # 是否为搜索字段进行判断
                label = field['label']  # 获取字段标签
                widget = None
                if field['type'] == 'text':
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'number':
                    widget = QLineEdit()  # 创建数字输入框
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'datetime':
                    widget = QDateTimeEdit()
                    widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                    widget.setCalendarPopup(True)  # 启用弹出式日历选择
                elif field['type'] == 'select':
                    widget = QComboBox()
                    widget.currentIndexChanged.connect(self.search_data)  # 监听切换selection事件
                    for option in field['options']:
                        widget.addItem(option['label'], option['value'])
                elif field['type'] == 'switch':
                    # 使用QRadioButton
                    pass
                else:
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键

                self.search_widgets[field['name']] = widget  # 将输入框添加到字典
                self.search_group_layout.addWidget(QLabel(label))  # 添加标签到布局
                self.search_group_layout.addWidget(widget)  # 添加输入框到布局

        self.search_button = QPushButton("搜索")  # 创建搜索按钮
        self.search_button.clicked.connect(self.search_data)  # 连接搜索按钮信号
        self.reset_button = QPushButton("重置")  # 创建重置按钮
        self.reset_button.clicked.connect(self.reset_search)  # 连接重置按钮信号

        self.search_group_layout.addWidget(self.search_button)  # 添加搜索按钮到布局
        self.search_group_layout.addWidget(self.reset_button)  # 添加重置按钮到布局

    async def init_data_block(self):
        self.data_layout = QVBoxLayout()  # 创建垂直布局

        control_group = QGroupBox()  # 创建控制区组
        control_group_layout = QVBoxLayout()  # 创建垂直布局
        control_group.setLayout(control_group_layout)  # 设置组布局

        self.control_layout = QHBoxLayout()  # 创建水平布局
        self.add_button = QPushButton("新增")  # 创建新增按钮
        self.add_button.clicked.connect(self.add_data)  # 连接新增按钮信号
        self.delete_button = QPushButton("删除")  # 创建删除按钮
        self.delete_button.clicked.connect(self.delete_data)  # 连接删除按钮信号
        self.clear_button = QPushButton("清空")  # 创建清空按钮
        self.clear_button.clicked.connect(self.clear_data)  # 连接清空按钮信号
        self.export_button = QPushButton("导出")  # 创建导出按钮

        self.export_button.clicked.connect(self.export_data)  # 连接导出按钮信号
        self.hide_search_button = QPushButton("隐藏搜索块")  # 创建隐藏搜索块按钮
        self.hide_search_button.clicked.connect(self.toggle_search_block)  # 连接隐藏搜索块按钮信号
        self.refresh_button = QPushButton("刷新数据")  # 创建刷新数据按钮
        self.refresh_button.clicked.connect(self.load_data)  # 连接刷新数据按钮信号

        self.control_layout.addWidget(self.add_button)  # 添加新增按钮到布局
        self.control_layout.addWidget(self.delete_button)  # 添加删除按钮到布局
        self.control_layout.addWidget(self.clear_button)  # 添加清空按钮到布局
        self.control_layout.addWidget(self.export_button)  # 添加导出按钮到布局
        # 加上钻取的按钮
        # for index, d in enumerate(self.drill):
        #     drill_button = QPushButton(d['button_name'])  # 创建钻取按钮
        #     drill_button.clicked.connect(lambda checked, d=d: self.drill_data_rows(d))  # 连接批量钻取按钮信号
        #     self.control_layout.addWidget(drill_button)  # 添加导出按钮到布局

        self.control_layout.addStretch()  # 添加伸缩项
        self.control_layout.addWidget(self.hide_search_button)  # 添加隐藏搜索块按钮到布局
        self.control_layout.addWidget(self.refresh_button)  # 添加刷新数据按钮到布局

        control_group_layout.addLayout(self.control_layout)  # 将控制布局添加到数据布局

        self.data_layout.addWidget(control_group)  # 将控制布局添加到数据布局

        self.table_widget = QTableWidget()  # 创建表格部件
        self.table_widget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)  # 设置表格不可编辑
        # "详情", "编辑", "删除"为基础按钮,外加钻取按钮
        self.table_widget.setColumnCount(len(self.config['fields']) + 3 + len(self.drill))  # 设置列数
        labels = []
        for index, field in enumerate(self.config['fields']):
            labels.append(field['label'])
            if field['hidden']:
                self.table_widget.setColumnHidden(index, True)

        headers = labels + ["详情", "编辑", "删除"] + [d['button_name'] for d in self.drill]  # 设置表头

        width_list = []
        for field in self.config['fields']:
            if "width" in field:
                width_list.append(field['width'])
            else:
                width_list.append(0)
        # "详情", "编辑", "删除"
        width_list.append(60)
        width_list.append(60)
        width_list.append(60)
        # 钻取按钮
        for d in self.drill:
            if hasattr(d, "width"):
                width_list.append(d['width'])
            else:
                width_list.append(0)
        self.table_widget.setHorizontalHeaderLabels(headers)  # 设置表头标签
        self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)  # 设置列宽手动调节
        for index, header in enumerate(headers):
            if width_list[index] == 0:
                # 设置自动拉伸以适应窗口
                self.table_widget.horizontalHeader().setSectionResizeMode(index, QHeaderView.ResizeMode.Stretch)
            else:
                self.table_widget.horizontalHeader().resizeSection(index, width_list[index])
        # self.table_widget.setSortingEnabled(True)  # 启用排序

        self.data_layout.addWidget(self.table_widget)  # 将表格部件添加到数据布局

        pagination_group = QGroupBox()  # 创建页码区组
        pagination_group_layout = QVBoxLayout()  # 创建垂直布局
        pagination_group.setLayout(pagination_group_layout)  # 设置组布局

        self.pagination_layout = QHBoxLayout()  # 创建水平布局
        self.current_page_label = QLabel("当前页码: 1")  # 创建当前页码标签
        self.total_records_label = QLabel("总记录数: 0")  # 创建总记录数标签
        self.page_size_combo = QComboBox()  # 创建每页显示条数下拉框
        self.page_size_combo.addItems(["10", "20", "50", "100"])  # 添加选项
        self.page_size_combo.currentIndexChanged.connect(self.load_data)  # 连接下拉框信号
        self.prev_button = QPushButton("上一页")  # 创建上一页按钮
        self.prev_button.clicked.connect(self.prev_page)  # 连接上一页按钮信号
        self.next_button = QPushButton("下一页")  # 创建下一页按钮
        self.next_button.clicked.connect(self.next_page)  # 连接下一页按钮信号
        self.page_input = QLineEdit("1")  # 创建页码输入框
        self.page_input.setValidator(QRegularExpressionValidator(r'^(?:[0-9]|[1-9][0-9]|100)$'))  # 限制数字范围为 0 到 100
        self.page_input.textChanged.connect(
            lambda text: self.page_input.setText("1") if text == "" else None)  # 置空时自动设置为 1
        self.page_input.setFixedWidth(30)  # 设置输入框宽度
        self.jump_button = QPushButton("跳转")  # 创建跳转按钮
        self.jump_button.clicked.connect(self.jump_to_page)  # 连接跳转按钮信号

        self.pagination_layout.addWidget(self.current_page_label)  # 添加当前页码标签到布局
        self.pagination_layout.addWidget(self.total_records_label)  # 添加总记录数标签到布局
        self.pagination_layout.addWidget(QLabel("每页显示:"))  # 添加标签到布局
        self.pagination_layout.addWidget(self.page_size_combo)  # 添加下拉框到布局
        self.pagination_layout.addStretch()  # 添加伸缩项
        self.pagination_layout.addWidget(self.prev_button)  # 添加上一页按钮到布局
        self.pagination_layout.addWidget(self.next_button)  # 添加下一页按钮到布局
        self.pagination_layout.addWidget(self.page_input)  # 添加页码输入框到布局
        self.pagination_layout.addWidget(self.jump_button)  # 添加跳转按钮到布局

        pagination_group_layout.addLayout(self.pagination_layout)  # 将分页布局添加到数据布局

        self.data_layout.addWidget(pagination_group)  # 将分页布局添加到数据布局

        self.layout.addLayout(self.data_layout)  # 将数据布局添加到主布局

    @asyncSlot()
    async def load_data(self):
        pool = await create_pool()
        try:
            page_size = int(self.page_size_combo.currentText())  # 获取每页显示条数
            page = int(self.page_input.text())  # 获取当前页码
            conditions = {}  # 搜索条件字典
            if self.foreign_key and self.foreign_value:
                conditions[self.foreign_key] = self.foreign_value
            query = await select_list(pool, self.table_name, condition=conditions,
                                      order_by=self.order_by, order_direction=["desc" for i in self.order_by])
            total_records = len(query)  # 获取总记录数
            self.total_records_label.setText(f"总记录数: {total_records}")  # 更新总记录数标签

            data = await select_page(pool, self.table_name, condition=conditions, page=page, page_size=page_size,
                                     order_by=self.order_by, order_direction=["desc" for i in self.order_by])
            self.table_widget.clear()
            self.table_widget.setRowCount(0)  # 清空表格
            for row_data in data:
                row = self.table_widget.rowCount()  # 获取当前行数
                self.table_widget.insertRow(row)  # 插入新行
                for i, field in enumerate(self.config['fields']):
                    if field['name'] in row_data:
                        if field['type'] == 'select':
                            mapping = {}
                            for options in field['options']:
                                mapping[options['value']] = options['label']
                            item = QTableWidgetItem(str(mapping[row_data[field['name']]]))  # 创建表格项
                            self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
                            if "align" in field:
                                if field["align"] == "right":
                                    item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
                                elif field["align"] == "center":
                                    item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
                                elif field["align"] == "left":
                                    item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
                        elif field['type'] == 'text_edit':
                            text_edit = QTextEdit(row_data[field['name']])
                            self.table_widget.setCellWidget(row, i, text_edit)  # 设置详情按钮到单元格
                            self.table_widget.setRowHeight(row, 80)
                        else:
                            item = QTableWidgetItem(str(row_data[field['name']]))  # 创建表格项
                            self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
                            if "align" in field:
                                if field["align"] == "right":
                                    item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
                                elif field["align"] == "center":
                                    item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
                                elif field["align"] == "left":
                                    item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
                    else:
                        item = QTableWidgetItem("")  # 创建表格项
                        self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
                        if "align" in field:
                            if field["align"] == "right":
                                item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
                            elif field["align"] == "center":
                                item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
                            elif field["align"] == "left":
                                item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
                detail_button = QPushButton("详情")  # 创建详情按钮
                detail_button.clicked.connect(lambda _, r=row: self.show_detail(r))  # 连接详情按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']), detail_button)  # 设置详情按钮到单元格
                edit_button = QPushButton("编辑")  # 创建编辑按钮
                edit_button.clicked.connect(lambda _, r=row: self.edit_data(r))  # 连接编辑按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']) + 1, edit_button)  # 设置编辑按钮到单元格
                delete_button = QPushButton("删除")  # 创建删除按钮
                delete_button.clicked.connect(lambda _, r=row: self.delete_data_row(r))  # 连接删除按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']) + 2, delete_button)  # 设置删除按钮到单元格
                # 数据钻取按钮
                for index, d in enumerate(self.drill):
                    drill_button = QPushButton(d['button_name'])  # 创建钻取按钮
                    drill_button.clicked.connect(lambda checked, r=row, d=d: self.drill_data_row(r, d))  # 连接删除按钮信号
                    self.table_widget.setCellWidget(row, len(self.config['fields']) + 2 + index + 1,
                                                    drill_button)  # 设置删除按钮到单元格

            self.current_page_label.setText(f"当前页码: {page}")  # 更新当前页码标签

        finally:
            await close_pool(pool)

    @asyncSlot()
    async def search_data(self):
        pool = await create_pool()
        try:
            if not hasattr(self, 'table_widget'):
                return  # 如果没有表格,则不执行操作
            conditions = {}  # 搜索条件字典
            if self.foreign_key and self.foreign_value:
                conditions[self.foreign_key] = self.foreign_value
            for name, widget in self.search_widgets.items():
                if isinstance(widget, QComboBox):
                    # value = widget.currentText()  # 获取下拉框选中项
                    value = widget.currentData()

                elif isinstance(widget, QDateTimeEdit):
                    value = widget.text()
                    if widget.text() == '2000-01-01 00:00:00':
                        value = None
                else:
                    value = widget.text()  # 获取输入框文本
                if value:
                    if self.field_type_mapping[name] == "select":
                        conditions[name] = value
                    elif self.field_type_mapping[name] == "number":
                        conditions[name] = value
                    else:
                        # 如果需要模糊搜索,使用 '%' 通配符
                        conditions[name] = {"like": f"%{value}%"}

            page_size = int(self.page_size_combo.currentText())  # 获取每页显示条数
            page = int(self.page_input.text())  # 获取当前页码
            offset = (page - 1) * page_size  # 计算偏移量

            data = await select_page(pool, self.table_name, condition=conditions, page=page, page_size=page_size,
                                     order_by=self.order_by, order_direction=["desc" for i in self.order_by])
            self.table_widget.setRowCount(0)  # 清空表格
            for row_data in data:
                row = self.table_widget.rowCount()  # 获取当前行数
                self.table_widget.insertRow(row)  # 插入新行
                for i, field in enumerate(self.config['fields']):
                    if field['type'] == 'select':
                        mapping = {}
                        for options in field['options']:
                            mapping[options['value']] = options['label']
                        item = QTableWidgetItem(str(mapping[row_data[field['name']]]))  # 创建表格项
                        self.table_widget.setItem(row, i, item)  # 设置表格项到单元格
                    else:
                        item = QTableWidgetItem(str(row_data[field['name']]))  # 创建表格项
                        self.table_widget.setItem(row, i, item)  # 设置表格项到单元格

                detail_button = QPushButton("详情")  # 创建详情按钮
                detail_button.clicked.connect(lambda _, r=row: self.show_detail(r))  # 连接详情按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']), detail_button)  # 设置详情按钮到单元格
                edit_button = QPushButton("编辑")  # 创建编辑按钮
                edit_button.clicked.connect(lambda _, r=row: self.edit_data(r))  # 连接编辑按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']) + 1, edit_button)  # 设置编辑按钮到单元格
                delete_button = QPushButton("删除")  # 创建删除按钮
                delete_button.clicked.connect(lambda _, r=row: self.delete_data_row(r))  # 连接删除按钮信号
                self.table_widget.setCellWidget(row, len(self.config['fields']) + 2, delete_button)  # 设置删除按钮到单元格
                # 数据钻取按钮
                for index, d in enumerate(self.drill):
                    drill_button = QPushButton(d['button_name'])  # 创建钻取按钮
                    drill_button.clicked.connect(lambda checked, r=row, d=d: self.drill_data_row(r, d))  # 连接删除按钮信号
                    self.table_widget.setCellWidget(row, len(self.config['fields']) + 2 + index + 1,
                                                    drill_button)  # 设置删除按钮到单元格
        finally:
            await close_pool(pool)

    @asyncSlot()
    async def reset_search(self):
        await self.init_search_block()
        await self.load_data()  # 重新加载数据

    @asyncSlot()
    async def add_data(self):
        pool = await create_pool()
        try:
            dialog = QDialog(self)  # 创建对话框
            dialog.setWindowTitle("新增数据")  # 设置对话框标题
            form_layout = QFormLayout(dialog)  # 创建表单布局
            dialog.setLayout(form_layout)  # 设置对话框布局

            def validate_and_accept(dialog, form_widgets):
                invalid_widgets = []
                for name, widget in form_widgets.items():
                    field = next(f for f in self.config['fields'] if f['name'] == name)
                    if 'required' in field and field['required']:
                        if isinstance(widget, QLineEdit) and not widget.text():
                            invalid_widgets.append(widget)
                            widget.setStyleSheet("border: 2px solid red;")
                        else:
                            widget.setStyleSheet("")
                    if 'regex' in field:
                        validator = widget.validator()
                        if validator:
                            state, _, _ = validator.validate(widget.text(), 0)
                            if state != QRegularExpressionValidator.State.Acceptable:
                                invalid_widgets.append(widget)
                                widget.setStyleSheet("border: 2px solid red;")
                            else:
                                widget.setStyleSheet("")

                if invalid_widgets:
                    for widget in invalid_widgets:
                        print("发生动画")
                else:
                    dialog.accept()

            form_widgets = {}  # 表单部件字典
            for field in self.config['fields']:
                label = field['label']  # 获取字段标签
                if 'required' in field and field['required']:
                    label = QLabel(f"<span style='color:red;'>*</span>{label}")
                if field['type'] == 'id':
                    pass
                elif field['type'] == 'text':
                    widget = QLineEdit()  # 创建输入框
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))
                elif field['type'] == 'number':
                    widget = QLineEdit()  # 创建数字输入框
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))
                elif field['type'] == 'datetime':
                    widget = QDateTimeEdit()
                    widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                    widget.setCalendarPopup(True)  # 启用弹出式日历选择
                    widget.setDateTime(QDateTime.currentDateTime())
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setDateTime(self.foreign_value)
                elif field['type'] == 'select':
                    widget = QComboBox()
                    for option in field['options']:
                        widget.addItem(option['label'], option['value'])
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setEditable(False)
                        widget.setCurrentIndex(self.foreign_value)
                elif field['type'] == 'file':
                    widget = FilePicker(title="选取文件", filter="All Files (*)")
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                elif field['type'] == 'folder':
                    widget = FolderPicker(title="选取文件夹")
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                else:
                    widget = QLineEdit()  # 创建输入框
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))

            buttons = QDialogButtonBox(
                QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
            buttons.accepted.connect(lambda: validate_and_accept(dialog, form_widgets))  # 连接确认按钮信号
            buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
            form_layout.addRow(buttons)  # 添加按钮框到布局

            if dialog.exec() == QDialog.DialogCode.Accepted:
                data = {}
                for name, widget in form_widgets.items():
                    if isinstance(widget, QLineEdit):
                        if self.field_type_mapping[name] == "number":
                            data[name] = int(widget.text())
                        else:
                            data[name] = widget.text()
                        validator = QRegularExpressionValidator(r'^\d*$')
                        widget.setValidator(validator)
                    elif isinstance(widget, QSpinBox):
                        data[name] = widget.value()
                    elif isinstance(widget, QDateTimeEdit):
                        data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                    elif isinstance(widget, QComboBox):
                        data[name] = widget.currentData()
                    elif isinstance(widget, FilePicker):
                        data[name] = widget.line_edit.text()
                    elif isinstance(widget, FolderPicker):
                        data[name] = widget.line_edit.text()
                # 插入数据
                await insert(pool=pool, table_name=self.table_name, data=data)
                await self.load_data()  # 重新加载数据
        finally:
            await close_pool(pool)

    @asyncSlot()
    async def delete_data(self):
        pool = await create_pool()
        try:
            # 删除数据逻辑
            selected_rows = set()
            for item in self.table_widget.selectedItems():
                selected_rows.add(item.row())

            if not selected_rows:
                QMessageBox.warning(self, "警告", "请选择要删除的行")
                return

            confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                          QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
            if confirm == QMessageBox.StandardButton.Yes:
                for row in sorted(selected_rows, reverse=True):
                    # 获取要删除的记录的ID
                    id_item = self.table_widget.item(row, 0)  # 假设ID在第一列
                    if id_item is not None:
                        record_id = int(id_item.text())
                        # 从数据库中删除记录
                        await delete_by_ids(pool, self.table_name, [record_id])
                        # 从表格中删除行
                        self.table_widget.removeRow(row)
        finally:
            await close_pool(pool)

    @asyncSlot()
    async def clear_data(self):
        confirm = QMessageBox.warning(self, "确认清空", "你确定要清空所有数据吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            pool = await create_pool()
            try:
                # 清空数据表
                await truncate_table(pool, self.table_name)
                await self.load_data()  # 重新加载数据
            finally:
                await close_pool(pool)

    @asyncSlot()
    async def export_data(self):
        pool = await create_pool()
        try:
            data = await select_list(pool, self.table_name)
            self.export_thread = ExportThread(data)
            self.export_thread.finished.connect(self.export_finished)
            self.export_thread.start()
        finally:
            await close_pool(pool)

    @asyncSlot()
    async def export_finished(self):
        QMessageBox.information(self, "导出完成", "数据已成功导出到 exported_data.xlsx")

    @asyncSlot()
    async def toggle_search_block(self):
        if self.search_group.isVisible():
            self.search_group.setVisible(False)
            self.hide_search_button.setText("显示搜索块")
        else:
            self.search_group.setVisible(True)
            self.hide_search_button.setText("隐藏搜索块")

    @asyncSlot()
    async def prev_page(self):
        current_page = int(self.page_input.text())  # 获取当前页码
        if current_page > 1:
            self.page_input.setText(str(current_page - 1))  # 更新页码
            await self.load_data()  # 重新加载数据

    @asyncSlot()
    async def next_page(self):
        current_page = int(self.page_input.text())  # 获取当前页码
        page_size = int(self.page_size_combo.currentText())  # 获取每页显示条数
        total_records = int(self.total_records_label.text().split(": ")[1])  # 获取总记录数
        if (current_page * page_size) < total_records:
            self.page_input.setText(str(current_page + 1))  # 更新页码
            await self.load_data()  # 重新加载数据

    @asyncSlot()
    async def jump_to_page(self):
        await self.load_data()  # 重新加载数据

    @asyncSlot()
    async def show_detail(self, row):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("数据详情")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        for i, field in enumerate(self.config['fields']):
            label = field['label']  # 获取字段标签
            value = self.table_widget.item(row, i).text()  # 获取表格项文本
            form_layout.addRow(label, QLabel(value))  # 添加标签和值到布局

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        dialog.exec()  # 显示对话框

    @asyncSlot()
    async def edit_data(self, row):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("编辑数据")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        form_widgets = {}  # 表单部件字典
        for i, field in enumerate(self.config['fields']):
            label = field['label']  # 获取字段标签
            value = self.table_widget.item(row, i).text()  # 获取表格项文本
            if field['type'] == 'id':
                widget = QLineEdit(value)  # 创建标签框,ID不可编辑
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                widget.setReadOnly(True)
            elif field['type'] == 'text':
                widget = QLineEdit(value)  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'number':
                widget = QLineEdit(value)  # 创建数字输入框
                validator = QRegularExpressionValidator(r'^\d*$')
                widget.setValidator(validator)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))
            elif field['type'] == 'datetime':
                widget = QDateTimeEdit()
                widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                widget.setCalendarPopup(True)  # 启用弹出式日历选择
                widget.setDateTime(QDateTime.fromString(value, "yyyy-MM-dd hh:mm:ss"))
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setDateTime(self.foreign_value)
            elif field['type'] == 'select':
                widget = QComboBox()
                for option in field['options']:
                    widget.addItem(option['label'], option['value'])
                index = widget.findText(value)
                widget.setCurrentIndex(index)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setEditable(False)
                    widget.setCurrentIndex(self.foreign_value)
            elif field['type'] == 'file':
                widget = FilePicker(title="选取文件", filter="All Files (*)")
                widget.line_edit.setText(value)
                widget.line_edit.setReadOnly(True)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局

            elif field['type'] == 'folder':
                widget = FolderPicker(title="选取文件夹")
                widget.line_edit.setText(value)
                widget.line_edit.setReadOnly(True)
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局

            else:
                widget = QLineEdit(value)  # 创建输入框
                form_widgets[field['name']] = widget  # 将部件添加到字典
                form_layout.addRow(label, widget)  # 添加标签和部件到布局
                if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                    widget.setReadOnly(True)
                    widget.setText(str(self.foreign_value))

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        if dialog.exec() == QDialog.DialogCode.Accepted:
            data = {}
            for name, widget in form_widgets.items():
                if isinstance(widget, QLineEdit) or isinstance(widget, QLabel):
                    data[name] = widget.text()
                elif isinstance(widget, QSpinBox):
                    data[name] = widget.value()
                elif isinstance(widget, QDateTimeEdit):
                    data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                elif isinstance(widget, QComboBox):
                    data[name] = widget.currentData()
                elif isinstance(widget, FilePicker):
                    data[name] = widget.line_edit.text()
                elif isinstance(widget, FolderPicker):
                    data[name] = widget.line_edit.text()
            pool = await create_pool()
            try:
                # 更新数据
                await update_by_id(pool, self.table_name, data)
            finally:
                await close_pool(pool)
            await self.load_data()  # 重新加载数据

    @asyncSlot()
    async def delete_data_row(self, row):
        confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            pool = await create_pool()
            try:
                # 获取要删除的记录的ID
                id_item = self.table_widget.item(row, 0)  # 假设ID在第一列
                if id_item is not None:
                    record_id = int(id_item.text())
                    # 从数据库中删除记录
                    await delete_by_ids(pool, self.table_name, [record_id])
                    # 从表格中删除行
                    self.table_widget.removeRow(row)
            finally:
                await close_pool(pool)

    @asyncSlot()
    async def drill_data_rows(self, drill: dict):
        selected_rows = set()
        for item in self.table_widget.selectedItems():
            selected_rows.add(item.row())

        if not selected_rows:
            QMessageBox.warning(self, "警告", "请选择要执行的行")
            return

        confirm = QMessageBox.warning(self, "确认删除", "你确定要执行选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            for row in sorted(selected_rows, reverse=False):
                # 获取要处理的记录的ID
                id_item = self.table_widget.item(row, 0)  # 假设ID在第一列
                if id_item is not None:
                    await self.drill_data_row(row, drill, False)

    @asyncSlot()
    async def drill_data_row(self, row: int, drill: dict, single_mode: bool = True):
        """
        row:行
        drill:钻取信息
        single_mode:单例模式
        """
        pool = await create_pool()
        try:
            # 获取记录的ID
            id_item = self.table_widget.item(row, 0)  # 假设ID在第一列
            if id_item is not None:
                record_id = int(id_item.text())  # 数据ID
                record = await select_by_id(pool, self.table_name, record_id)
                button_name = drill['button_name']
                if 'form_config_json' in drill:
                    # 路径是相对于父json文件
                    dir_path = self.form_json.replace(os.path.basename(self.form_json), "")
                    form_config_json_path: str = drill['form_config_json']
                    if form_config_json_path.startswith("./"):
                        form_config_json_path = form_config_json_path[2:]
                    drill_form_config_json_path = os.path.join(dir_path, form_config_json_path)
                    foreign_key = drill['foreign_key']
                    drill_form_config_json_data = json.load(open(drill_form_config_json_path, 'r', encoding='utf-8'))
                    window_id = f"crud_widget_{drill_form_config_json_data['id']}"

                    # 防止多次点击打开多个窗口
                    if not single_mode:
                        if 'parent_id' in [f['name'] for f in drill_form_config_json_data['fields']]:
                            # 这里只能采取self.id:record,如果是固定的record会覆盖,没办法把每一级的数据带过来
                            widget: TreeWidget = TreeWidget(parent=self,
                                                            parent_data={**self.parent_data, self.id: record},
                                                            form_json=drill_form_config_json_path,
                                                            foreign_key=foreign_key,
                                                            foreign_value=record_id)
                        else:
                            # 这里只能采取self.id:record,如果是固定的record会覆盖,没办法把每一级的数据带过来
                            widget: CrudWidget = CrudWidget(parent=self,
                                                            parent_data={**self.parent_data, self.id: record},
                                                            form_json=drill_form_config_json_path,
                                                            foreign_key=foreign_key,
                                                            foreign_value=record_id)
                            widget.isVisible()
                            widget.isEnabled()
                        setattr(self, window_id, widget)
                        widget.show()
                    else:
                        try:
                            # 每次重现的时候将参数强制赋值进去
                            widget: CrudWidget | TreeWidget = getattr(self, window_id)
                            setattr(widget, "parent", self)
                            setattr(widget, "parent_data", {**self.parent_data, self.id: record})
                            setattr(widget, "form_json", drill_form_config_json_path)
                            setattr(widget, "foreign_key", foreign_key)
                            setattr(widget, "foreign_value", record_id)
                            widget.show()
                            widget.activateWindow()  # 激活窗口
                            widget.raise_()  # 将窗口提升到最前面
                        except:
                            if 'parent_id' in [f['name'] for f in drill_form_config_json_data['fields']]:
                                parent_data = {**self.parent_data, self.id: record}
                                widget: TreeWidget = TreeWidget(parent=self, parent_data=parent_data,
                                                                form_json=drill_form_config_json_path,
                                                                foreign_key=foreign_key,
                                                                foreign_value=record_id)
                            else:
                                parent_data = {**self.parent_data, self.id: record}
                                widget: CrudWidget = CrudWidget(parent=self,
                                                                parent_data=parent_data,
                                                                form_json=drill_form_config_json_path,
                                                                foreign_key=foreign_key,
                                                                foreign_value=record_id)
                                widget.isVisible()
                                widget.isEnabled()
                            setattr(self, window_id, widget)
                            widget.show()

                elif 'function' in drill:
                    function_name = drill['function']
                    parent = self
                    while True:
                        if hasattr(parent, "parent"):
                            if hasattr(parent.parent, function_name):
                                function = getattr(parent.parent, function_name)
                                # 将方法设置进触发类中。
                                setattr(self, function_name, function)
                                """
                                这里只能采取self.id:record,如果是固定的record会覆盖,没办法把每一级的数据带过来,所以带过去的数据格式:
                                {
                                    table_id: record
                                    ...
                                }
                                即:key是json表的id,value是行数据
                                """
                                getattr(self, function_name)(self, {**self.parent_data, self.id: record})
                                return
                            else:
                                parent = parent.parent
                        else:
                            break

                    raise Exception(f"{function_name} 方法未在parent中实现")
        finally:
            await close_pool(pool)

    def showEvent(self, event):
        """
        这个函数必须重写一下。子窗口关闭后,引用还在上一层存着,并不会销毁,而是隐藏。再次显示并不会初始化。所以在显示的时候我们手动初始化一下。
        """
        super().showEvent(event)
        # 加载数据
        asyncio.ensure_future(self.load_data())


class TreeWidget(QWidget):
    def __init__(self, parent: QWidget, parent_data: dict, form_json: str | dict = "./tree_form_config.json",
                 foreign_key=None,
                 foreign_value=None):
        """
        :param form_json 表单文件
        :param foreign_key 外键名称
        :param foreign_value 外键值
        """
        super().__init__()
        self.parent = parent
        self.parent_data = parent_data
        self.form_json = form_json
        self.foreign_key = foreign_key
        self.foreign_value = foreign_value
        # 初始化解析表单配置
        try:
            with open(form_json, 'r', encoding='utf-8') as f:
                self.config = json.load(f)  # 加载表单配置
        except FileNotFoundError:
            raise ValueError(f"表单配置文件未找到:{self.config}")
        if 'id' not in self.config:
            raise ValueError("表单配置文件格式错误【无id参数】")

        if 'fields' not in self.config:
            raise ValueError("表单配置文件格式错误【无fields参数】")

        if 'title' not in self.config:
            raise ValueError("表单配置文件格式错误【无title参数】")

        if 'database' not in self.config:
            raise ValueError("表单配置文件格式错误【无database参数】")

        if 'table' not in self.config:
            raise ValueError("表单配置文件格式错误【无table参数】")

        fields = self.config['fields']
        if type(fields) is not list:
            raise ValueError("表单配置文件格式错误【fields必须是list类型】")
        for field in fields:
            if 'name' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少name】")
            if 'label' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少label】")
            if 'type' not in field:
                raise ValueError("表单配置文件格式错误【字段缺少type】")
        if 'parent_id' not in [f['name'] for f in fields]:
            raise ValueError("表单配置文件格式错误【fields缺少parent_id】")

        self.id = self.config['id']
        self.title = self.config['title']
        self.database = self.config['database']
        self.background = self.config['background'] if 'background' in self.config else '#ffffff'
        self.geometry = self.config['geometry'] if 'geometry' in self.config else [100, 100, 1000, 600]
        self.table_name = self.config['table']
        self.fields = self.config['fields']
        # 字段名与类型的映射
        self.field_type_mapping = {field['name']: field['type'] for field in fields}
        # 排序字段
        order_fields = sorted([field for field in fields if "order_by" in field], key=lambda x: x["order_by"])
        self.order_by = [field['name'] for field in order_fields]

        self.drill = self.config['drill']
        self.setWindowTitle(self.title)  # 设置窗口标题
        self.setGeometry(*self.geometry)
        self.setStyleSheet("""
                            QLineEdit, QSpinBox {
                                padding: 5px;
                                border: 1px solid #d0d0d0;
                                border-radius: 5px;
                            }

                            QLabel {
                                font-size: 14px;
                                color: #333333;
                            }
                        """)

        self.table_name = self.config['table']
        # self.adjustSize()
        # 在这里引入背景插件并显示
        self.custom_main_window = BackgroundWidget(self, False)
        self.custom_main_window.show()
        asyncio.ensure_future(self.init_ui())

    async def init_ui(self):
        self.layout = QVBoxLayout(self)  # 创建垂直布局
        await self.init_search_block()  # 初始化搜索块
        await self.init_data_block()  # 初始化数据块
        await self.load_data()  # 加载数据

    async def init_search_block(self):
        """初始化搜索块"""
        if hasattr(self, 'search_group') and hasattr(self, 'search_group_layout'):
            while self.search_group_layout.count():
                item = self.search_group_layout.takeAt(0)
                widget = item.widget()
                if widget is not None:
                    widget.deleteLater()
        else:

            self.search_group = QGroupBox()  # 创建搜索区组
            self.layout.addWidget(self.search_group)  # 将搜索布局添加到主布局
            self.search_group_layout = QHBoxLayout()  # 创建水平布局
            self.search_group.setLayout(self.search_group_layout)  # 设置组布局
        self.search_widgets = {}  # 搜索部件字典

        for field in self.config['fields']:
            if field['is_search']:  # 是否为搜索字段进行判断
                label = field['label']  # 获取字段标签
                widget = None
                if field['type'] == 'text':
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'number':
                    widget = QLineEdit()  # 创建数字输入框
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                    widget.returnPressed.connect(self.search_data)  # 监听回车键
                elif field['type'] == 'datetime':
                    widget = QDateTimeEdit()
                    widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                    widget.setCalendarPopup(True)  # 启用弹出式日历选择
                elif field['type'] == 'select':
                    widget = QComboBox()
                    widget.currentIndexChanged.connect(self.search_data)  # 监听切换selection事件
                    await self.update_parent_options(widget)
                else:
                    widget = QLineEdit()  # 创建输入框
                    widget.returnPressed.connect(self.search_data)  # 监听回车键

                self.search_widgets[field['name']] = widget  # 将输入框添加到字典
                self.search_group_layout.addWidget(QLabel(label))  # 添加标签到布局
                self.search_group_layout.addWidget(widget)  # 添加输入框到布局

        self.search_button = QPushButton("搜索")  # 创建搜索按钮
        self.search_button.clicked.connect(self.search_data)  # 连接搜索按钮信号
        self.reset_button = QPushButton("重置")  # 创建重置按钮
        self.reset_button.clicked.connect(self.reset_search)  # 连接重置按钮信号

        self.search_group_layout.addWidget(self.search_button)  # 添加搜索按钮到布局
        self.search_group_layout.addWidget(self.reset_button)  # 添加重置按钮到布局

    async def init_data_block(self):
        self.data_layout = QVBoxLayout()  # 创建垂直布局

        control_group = QGroupBox()  # 创建控制区组
        control_group_layout = QVBoxLayout()  # 创建垂直布局
        control_group.setLayout(control_group_layout)  # 设置组布局

        self.control_layout = QHBoxLayout()  # 创建水平布局
        self.add_button = QPushButton("新增")  # 创建新增按钮
        self.add_button.clicked.connect(self.add_data)  # 连接新增按钮信号
        self.delete_button = QPushButton("删除")  # 创建删除按钮
        self.delete_button.clicked.connect(self.delete_data)  # 连接删除按钮信号
        self.clear_button = QPushButton("清空")  # 创建清空按钮
        self.clear_button.clicked.connect(self.clear_data)  # 连接清空按钮信号
        self.refresh_button = QPushButton("刷新数据")  # 创建刷新数据按钮
        self.refresh_button.clicked.connect(self.load_data)  # 连接刷新数据按钮信号
        # 初始状态为收起
        self.is_expanded = False
        self.expand_button = QPushButton("一键展开")  # 创建刷新数据按钮
        self.expand_button.clicked.connect(self.toggle_expand_collapse)  # 连接刷新数据按钮信号

        self.control_layout.addWidget(self.add_button)  # 添加新增按钮到布局
        self.control_layout.addWidget(self.delete_button)  # 添加删除按钮到布局
        self.control_layout.addWidget(self.clear_button)  # 添加清空按钮到布局
        self.control_layout.addStretch()  # 添加伸缩项
        self.control_layout.addWidget(self.expand_button)  # 添加展开数据按钮到布局
        self.control_layout.addWidget(self.refresh_button)  # 添加刷新数据按钮到布局

        control_group_layout.addLayout(self.control_layout)  # 将控制布局添加到数据布局

        self.data_layout.addWidget(control_group)  # 将控制布局添加到数据布局

        self.tree_widget = QTreeWidget()  # 创建树形部件
        # "详情", "编辑", "删除"为基础按钮,外加钻取按钮
        self.tree_widget.setColumnCount(len(self.config['fields']) + 3 + len(self.drill))  # 设置列数
        labels = []
        for index, field in enumerate(self.config['fields']):
            labels.append(field['label'])
            if field['hidden']:
                self.tree_widget.setColumnHidden(index, True)

        headers = labels + ["详情", "编辑", "删除"] + [d['button_name'] for d in self.drill]  # 设置表头
        self.tree_widget.setHeaderLabels(headers)  # 设置表头
        self.tree_widget.header().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)  # 设置列宽自动调整
        # self.tree_widget.setSortingEnabled(True)  # 启用排序

        self.data_layout.addWidget(self.tree_widget)  # 将树形部件添加到数据布局

        self.layout.addLayout(self.data_layout)  # 将数据布局添加到主布局

    @asyncSlot()
    async def load_data(self):
        pool = await create_pool()
        try:

            conditions = {}  # 搜索条件字典
            if self.foreign_key and self.foreign_value:
                conditions[self.foreign_key] = self.foreign_value
            data = await select_list(pool, self.table_name,
                                     condition=conditions,
                                     order_by=self.order_by,
                                     order_direction=["desc" for i in self.order_by])  # 查询数据
            self.tree_widget.clear()  # 清空树形部件
            await self.build_tree(data)  # 构建树形结构
            self.tree_widget.expandAll()  # 展开树结构
        finally:
            await close_pool(pool)

    async def build_tree(self, data):
        nodes = {}

        for row_data in data:
            item = []
            for field in self.config['fields']:
                if field['name'] in row_data:
                    item.append(str(row_data[field['name']]))
                else:
                    item.append(None)
            node = QTreeWidgetItem(item)
            nodes[row_data['id']] = node
        for row_data in data:
            if row_data['parent_id'] is None:
                self.tree_widget.addTopLevelItem(nodes[row_data['id']])
            else:
                parent_node = nodes.get(row_data['parent_id'])
                if parent_node is None:
                    parent_node = nodes.get(int(row_data['parent_id']))
                if parent_node is not None:
                    parent_node.addChild(nodes[row_data['id']])
                else:
                    self.tree_widget.addTopLevelItem(nodes[row_data['id']])
            await self.add_buttons(nodes[row_data['id']])

    async def add_buttons(self, node):
        detail_button = QPushButton("详情")  # 创建详情按钮
        detail_button.clicked.connect(lambda _, n=node: self.show_detail(n))  # 连接详情按钮信号
        self.tree_widget.setItemWidget(node, len(self.config['fields']), detail_button)  # 设置详情按钮到单元格
        edit_button = QPushButton("编辑")  # 创建编辑按钮
        edit_button.clicked.connect(lambda _, n=node: self.edit_data(n))  # 连接编辑按钮信号
        self.tree_widget.setItemWidget(node, len(self.config['fields']) + 1, edit_button)  # 设置编辑按钮到单元格
        delete_button = QPushButton("删除")  # 创建删除按钮
        delete_button.clicked.connect(lambda _, n=node: self.delete_data_node(n))  # 连接删除按钮信号
        self.tree_widget.setItemWidget(node, len(self.config['fields']) + 2, delete_button)  # 设置删除按钮到单元格
        # 数据钻取按钮
        for index, d in enumerate(self.drill):
            drill_button = QPushButton(d['button_name'])  # 创建钻取按钮
            drill_button.clicked.connect(lambda _, n=node: self.drill_data_row(n, d))  # 连接删除按钮信号
            self.tree_widget.setItemWidget(node, len(self.config['fields']) + 2 + index + 1, drill_button)  # 设置删除按钮到单元格

    @asyncSlot()
    async def search_data(self):
        pool = await create_pool()

        try:
            conditions = {}  # 搜索条件字典
            if self.foreign_key and self.foreign_value:
                conditions[self.foreign_key] = self.foreign_value
            for name, widget in self.search_widgets.items():
                if isinstance(widget, QComboBox):
                    value = widget.currentData()
                else:
                    value = widget.text()  # 获取输入框文本
                if value:
                    # conditions[name] = value  # 添加到搜索条件字典
                    # 如果需要模糊搜索,使用 '%' 通配符
                    if name == "parent_id":
                        conditions[name] = value
                    else:
                        conditions[name] = {"like": f"%{value}%"}

            if 'parent_id' in conditions and conditions['parent_id'] is not None:
                params = ""
                for index, (k, v) in enumerate(conditions.items()):
                    if isinstance(v, dict):
                        params = params + f" {k} {list(v.keys())[0]} :{k}"
                    else:
                        params = params + f" {k}=:{k}"
                    if index != len(conditions.items()) - 1:
                        params = params + " and"
                query = rf"""
                                            WITH RECURSIVE descendants AS (
                                                SELECT id, name, parent_id
                                                FROM {self.config['table']}
                                                WHERE {params}
                                                UNION ALL
                                                SELECT cdt.id, cdt.name, cdt.parent_id
                                                FROM {self.config['table']} cdt
                                                INNER JOIN descendants d ON cdt.parent_id = d.id
                                            )
                                            SELECT * FROM descendants
                                            """
                for k, v in conditions.items():
                    if isinstance(v, dict):
                        conditions[k] = v['like']
                data = await select_by_sql(pool=pool, sql=query, params=())
            else:
                data = await select_list(pool, self.table_name, condition=conditions,
                                         order_by=self.order_by, order_direction=["desc" for i in self.order_by])

            if hasattr(self, 'tree_widget'):
                self.tree_widget.clear()  # 清空树形部件
            else:
                self.tree_widget = QTreeWidget()  # 创建树形部件
            await self.build_tree(data)  # 构建树形结构
            self.tree_widget.expandAll()  # 展开树结构

        finally:
            await close_pool(pool)


    @asyncSlot()
    async def reset_search(self):
        await self.init_search_block()
        await self.load_data()  # 重新加载数据


    @asyncSlot()
    async def add_data(self):
        pool = await create_pool()
        try:
            dialog = QDialog(self)  # 创建对话框
            dialog.setWindowTitle("新增数据")  # 设置对话框标题
            form_layout = QFormLayout(dialog)  # 创建表单布局
            dialog.setLayout(form_layout)  # 设置对话框布局

            form_widgets = {}  # 表单部件字典
            for field in self.config['fields']:
                label = field['label']  # 获取字段标签
                if field['type'] == 'id':
                    pass
                elif field['type'] == 'text':
                    widget = QLineEdit()  # 创建输入框
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))
                elif field['type'] == 'number':
                    widget = QLineEdit()  # 创建数字输入框
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))
                elif field['type'] == 'datetime':
                    widget = QDateTimeEdit()
                    widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                    widget.setCalendarPopup(True)  # 启用弹出式日历选择
                    widget.setDateTime(QDateTime.currentDateTime())
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setDateTime(self.foreign_value)
                elif field['type'] == 'select':
                    widget = QComboBox()
                    await self.update_parent_options(widget)
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setEditable(False)
                        widget.setCurrentIndex(self.foreign_value)

                elif field['type'] == 'file':
                    widget = FilePicker(title="选取文件", filter="All Files (*)")
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局

                elif field['type'] == 'folder':
                    widget = FolderPicker(title="选取文件夹")
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局

                else:
                    widget = QLineEdit()  # 创建输入框
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))

            buttons = QDialogButtonBox(
                QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
            buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
            buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
            form_layout.addRow(buttons)  # 添加按钮框到布局

            if dialog.exec() == QDialog.DialogCode.Accepted:
                data = {}
                for name, widget in form_widgets.items():
                    if isinstance(widget, QLineEdit):
                        data[name] = widget.text()
                    elif isinstance(widget, QSpinBox):
                        data[name] = widget.value()
                    elif isinstance(widget, QDateTimeEdit):
                        data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                    elif isinstance(widget, QComboBox):
                        data[name] = widget.currentData()
                    elif isinstance(widget, FilePicker):
                        data[name] = widget.line_edit.text()
                    elif isinstance(widget, FolderPicker):
                        data[name] = widget.line_edit.text()
                await insert(pool=pool, table_name=self.table_name, data=data)
                await self.load_data()  # 重新加载数据
        finally:
            await close_pool(pool)


    @asyncSlot()
    async def delete_data(self):
        selected_items = self.tree_widget.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "警告", "请选择要删除的行")
            return

        confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            for item in selected_items:
                await self.delete_item(item)


    async def delete_item(self, item):
        pool = await create_pool()
        try:
            id_item = item.text(0)  # 假设ID在第一列
            if id_item is not None:
                record_id = int(id_item)
                # 从数据库中删除记录
                await delete_by_ids(pool, self.table_name, [record_id])
                parent = item.parent()
                if parent is None:
                    self.tree_widget.takeTopLevelItem(self.tree_widget.indexOfTopLevelItem(item))
                else:
                    parent.removeChild(item)
        finally:
            await close_pool(pool)


    @asyncSlot()
    async def toggle_expand_collapse(self):
        if self.is_expanded:
            self.tree_widget.collapseAll()
            self.expand_button.setText("一键展开")
        else:
            self.tree_widget.expandAll()
            self.expand_button.setText("一键收起")
        self.is_expanded = not self.is_expanded


    @asyncSlot()
    async def clear_data(self):
        confirm = QMessageBox.warning(self, "确认清空", "你确定要清空所有数据吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            pool = await create_pool()
            try:
                # 清空数据表
                await truncate_table(pool, self.table_name)
                await self.load_data()  # 重新加载数据
            finally:
                await close_pool(pool)


    async def update_parent_options(self, combo_box, self_id=None):
        pool = await create_pool()
        try:
            combo_box.clear()
            combo_box.addItem("请选择父节点", None)

            conditions = {}  # 搜索条件字典
            if self.foreign_key and self.foreign_value:
                conditions[self.foreign_key] = self.foreign_value

            data = await select_list(pool, self.table_name, condition=conditions,
                                     order_by=self.order_by, order_direction=["desc" for i in self.order_by])
            for row_data in data:
                # 有些时候父节点和子节点是同一个,所以需要排除这种情况
                if self_id is not None and str(row_data['id']) == str(self_id):
                    continue
                combo_box.addItem(row_data['name'], row_data['id'])
        finally:
            await close_pool(pool)


    @asyncSlot()
    async def show_detail(self, node):
        dialog = QDialog(self)  # 创建对话框
        dialog.setWindowTitle("数据详情")  # 设置对话框标题
        form_layout = QFormLayout(dialog)  # 创建表单布局
        dialog.setLayout(form_layout)  # 设置对话框布局

        for i, field in enumerate(self.config['fields']):
            label = field['label']  # 获取字段标签
            value = node.text(i)  # 获取表格项文本
            form_layout.addRow(label, QLabel(value))  # 添加标签和值到布局

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)  # 创建按钮框
        buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
        form_layout.addRow(buttons)  # 添加按钮框到布局

        dialog.exec()  # 显示对话框


    @asyncSlot()
    async def edit_data(self, node):
        pool = await create_pool()
        try:
            dialog = QDialog(self)  # 创建对话框
            dialog.setWindowTitle("编辑数据")  # 设置对话框标题
            form_layout = QFormLayout(dialog)  # 创建表单布局
            dialog.setLayout(form_layout)  # 设置对话框布局

            form_widgets = {}  # 表单部件字典
            for i, field in enumerate(self.config['fields']):
                label = field['label']  # 获取字段标签
                value = node.text(i)  # 获取节点文本
                if field['type'] == 'id':
                    widget = QLineEdit(value)  # 创建标签框,ID不可编辑
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    widget.setReadOnly(True)
                elif field['type'] == 'text':
                    widget = QLineEdit(value)  # 创建输入框
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))
                elif field['type'] == 'number':
                    widget = QLineEdit(value)  # 创建数字输入框
                    validator = QRegularExpressionValidator(r'^\d*$')
                    widget.setValidator(validator)
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))
                elif field['type'] == 'datetime':
                    widget = QDateTimeEdit()
                    widget.setDisplayFormat("yyyy-MM-dd hh:mm:ss")  # 设置显示格式
                    widget.setCalendarPopup(True)  # 启用弹出式日历选择
                    widget.setDateTime(QDateTime.fromString(value, "yyyy-MM-dd hh:mm:ss"))
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setDateTime(self.foreign_value)
                elif field['type'] == 'select':
                    widget = QComboBox()
                    await self.update_parent_options(widget, self_id=node.text(0))
                    if value and value != 'None':
                        index = widget.findData(int(value))
                        widget.setCurrentIndex(index)
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setEditable(False)
                        widget.setCurrentIndex(self.foreign_value)
                else:
                    widget = QLineEdit(value)  # 创建输入框
                    form_widgets[field['name']] = widget  # 将部件添加到字典
                    form_layout.addRow(label, widget)  # 添加标签和部件到布局
                    if field['name'] == self.foreign_key:  # 如果是外键字段,则隐藏输入框
                        widget.setReadOnly(True)
                        widget.setText(str(self.foreign_value))

            buttons = QDialogButtonBox(
                QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)  # 创建按钮框
            buttons.accepted.connect(dialog.accept)  # 连接确认按钮信号
            buttons.rejected.connect(dialog.reject)  # 连接取消按钮信号
            form_layout.addRow(buttons)  # 添加按钮框到布局

            if dialog.exec() == QDialog.DialogCode.Accepted:
                data = {}
                for name, widget in form_widgets.items():
                    if isinstance(widget, QLineEdit) or isinstance(widget, QLabel):
                        data[name] = widget.text()
                    elif isinstance(widget, QSpinBox):
                        data[name] = widget.value()
                    elif isinstance(widget, QDateTimeEdit):
                        data[name] = widget.dateTime().toString("yyyy-MM-dd HH:mm:ss")
                    elif isinstance(widget, QComboBox):
                        data[name] = widget.currentData()
                if str(data['parent_id']) == str(data['id']):
                    raise Exception("父节点ID不能等于自身ID")
                # 还有一种情况要排除掉,那就是两个节点,互为其父节点。
                one = await select_by_id(pool=pool, table_name=self.table_name, id=data['parent_id'])
                if one and one['parent_id'] == data['id']:
                    raise Exception("父节点不能为其子节点")
                await update_by_id(pool, table_name=self.table_name, data=data)
                await self.load_data()  # 重新加载数据
        finally:
            await close_pool(pool)


    @asyncSlot()
    async def delete_data_node(self, node):
        confirm = QMessageBox.warning(self, "确认删除", "你确定要删除选中的行吗?",
                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if confirm == QMessageBox.StandardButton.Yes:
            pool = await create_pool()
            try:
                id_item = node.text(0)  # 假设ID在第一列
                if id_item is not None:
                    record_id = int(id_item)
                    await delete_by_ids(pool, table_name=self.table_name, id_list=[record_id])
                    parent = node.parent()
                    if parent is None:
                        self.tree_widget.takeTopLevelItem(self.tree_widget.indexOfTopLevelItem(node))
                    else:
                        parent.removeChild(node)
            finally:
                await close_pool(pool)


    @asyncSlot()
    async def drill_data_row(self, node, drill: dict):
        pool = await create_pool()
        try:
            # 获取记录的ID
            record_id = node.text(0)  # 假设ID在第一列
            if record_id is not None:
                record = await select_by_id(pool, table_name=self.table_name, id=record_id)
                button_name = drill['button_name']
                if 'form_config_json' in drill:
                    dir_path = self.form_json.replace(os.path.basename(self.form_json), "")
                    drill_form_config_json_path = os.path.join(dir_path, drill['form_config_json'])
                    foreign_key = drill['foreign_key']
                    drill_form_config_json_data = json.load(open(drill_form_config_json_path, 'r', encoding='utf-8'))
                    window_id = f"crud_widget_{drill_form_config_json_data['id']}"
                    # 防止多次点击打开多个窗口
                    try:
                        # 每次重现的时候将参数强制赋值进去
                        widget = getattr(self, window_id)
                        setattr(widget, "parent", self)
                        setattr(widget, "parent_data", {**self.parent_data, self.id: record})
                        setattr(widget, "form_json", drill_form_config_json_path)
                        setattr(widget, "foreign_key", foreign_key)
                        setattr(widget, "foreign_value", record_id)
                        widget.show()
                        widget.activateWindow()  # 激活窗口
                        widget.raise_()  # 将窗口提升到最前面
                    except:
                        if 'parent_id' in [f['name'] for f in drill_form_config_json_data['fields']]:
                            parent_data = {**self.parent_data, self.id: record}
                            widget: TreeWidget = TreeWidget(parent=self, parent_data=parent_data,
                                                            form_json=drill_form_config_json_path,
                                                            foreign_key=foreign_key,
                                                            foreign_value=record_id)
                        else:
                            parent_data = {**self.parent_data, self.id: record}
                            widget: CrudWidget = CrudWidget(parent=self, parent_data=parent_data,
                                                            form_json=drill_form_config_json_path,
                                                            foreign_key=foreign_key,
                                                            foreign_value=record_id)
                            widget.isVisible()
                            widget.isEnabled()
                        setattr(self, window_id, widget)
                        widget.show()

                elif 'function' in drill:
                    function_name = drill['function']
                    if hasattr(self.parent, function_name):
                        getattr(self.parent, function_name)({**self.parent_data, self.id: record})
                    else:
                        raise Exception(f"{function_name} 方法未在parent中实现")

        finally:
            await close_pool(pool)


    def showEvent(self, event):
        """
        这个函数必须重写一下。子窗口关闭后,引用还在上一层存着,并不会销毁,而是隐藏。再次显示并不会初始化。所以在显示的时候我们手动初始化一下。
        """
        super().showEvent(event)
        asyncio.ensure_future(self.init_search_block())
        asyncio.ensure_future(self.load_data())


class ExistingWindow(QWidget):
    def __init__(self, parent=None):
        super(ExistingWindow, self).__init__(parent)
        self.setWindowTitle("Existing Window")
        self.resize(600, 400)
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)
        # 其他初始化代码
        self.button = QPushButton("Test")
        self.layout.addWidget(self.button)

        self.crud_widget = None  # 用于存储CrudWidget实例

        self.button.clicked.connect(self.btn_event)

    @asyncSlot()
    async def btn_event(self):
        # 防止多次点击打开多个窗口
        try:
            self.crud_widget.show()
            self.crud_widget.activateWindow()  # 激活窗口
            self.crud_widget.raise_()  # 将窗口提升到最前面
        except:
            self.crud_widget = CrudWidget(parent=self, parent_data={},
                                          form_json='crud_form_config.json')
            self.crud_widget.show()

    @staticmethod
    async def mind_extend_handle(subject, record):
        """
        :param subject 触发方法的窗口句柄
        :param record 触发方法时携带的参数
        """
        print(f"触发额外窗口,携带数据为{record}")


if __name__ == "__main__":
    # 创建主循环
    app = QApplication(sys.argv)

    # 创建异步事件循环
    loop = QEventLoop(app)
    asyncio.set_event_loop(loop)

    # 创建窗口
    window = ExistingWindow()
    # 显示窗口
    window.show()

    # 运行事件循环
    loop.run_forever()

三、效果图

<think>好的,我现在需要帮用户在PySide6中实现多进程处理。根据用户提供的引用,我需要先理解现有的资料,然后结合PySide6的特点给出解决方案。 首先,用户提供了三个引用。引用[1]提到Python的multiprocess库,可以使用Queue和Pipe进行进程间通信。引用[2]是关于PySide2中使用QProcess模块启动外部程序并传参的例子。引用[3]则是PySide6中使用多线程进行巡检的例子,涉及线程的退出和等待。 用户的问题是在PySide6中添加多进程支持。我需要考虑两种方法:使用Python原生的multiprocessing模块,或者使用PySide6自带的QProcess类。 对于第一种方法,multiprocessing模块是标准库的一部分,适合处理计算密集型任务。但需要注意,在PySide6的GUI线程中直接使用可能会引发问题,比如子进程修改GUI元素会导致崩溃,因为GUI工具包通常要求所有操作在主线程执行。因此,子进程应该通过信号与主线程通信,或者使用队列、管道传递数据,由主线程更新界面。 第二种方法是使用QProcess,这是Qt框架提供的类,用于启动和管理外部进程。适合需要调用其他可执行文件或脚本的情况。QProcess可以方便地传递参数,处理输入输出,并且与Qt的事件循环集成良好,避免阻塞UI。 接下来需要详细说明这两种方法的实现步骤。比如,使用multiprocessing时,如何创建进程、处理通信,以及如何与PySide6的信号槽结合。同时,要注意进程间不能直接共享GUI资源,必须通过安全的方式传递数据。例如,使用multiprocessing的Queue,然后在主线程中监听队列的变化,更新UI。 对于QProcess,需要展示如何创建QProcess实例,设置要执行的程序和参数,启动进程,并处理完成信号。比如,连接finished信号到一个槽函数,以便在进程结束时执行清理操作或获取输出。 还需要考虑错误处理,比如进程启动失败、超时等情况。此外,多进程之间的数据传递格式,如使用JSON或其他序列化方法,确保数据正确传输。 最后,总结两种方法的适用场景,帮助用户根据具体需求选择合适的方式。比如,计算密集型任务用multiprocessing,调用外部工具用QProcess。同时,强调避免在子进程中操作GUI组件,确保程序稳定性。 可能还需要提醒用户注意跨平台兼容性,比如在Windows和Linux下路径处理的差异,或者参数传递的不同。另外,资源管理方面,如及时释放完成的进程,防止内存泄漏。 现在需要将这些思考整理成结构化的回答,分步骤说明两种方法,并给出代码示例,确保符合用户的要求,比如正确使用$$...$$格式,引用标记等。同时生成相关问题,帮助用户进一步了解多进程处理的其他方面。</think>在PySide6中实现多进程处理主要有两种方法:使用Python标准库的$multiprocessing$模块或Qt框架的$QProcess$类。以下是具体实现步骤: --- ### 方法一:使用Python的multiprocessing模块(适合计算密集型任务) #### 1. 创建子进程 ```python from PySide6.QtCore import QObject, Signal from multiprocessing import Process, Queue # 自定义信号类用于跨进程通信 class ProcessSignals(QObject): result_ready = Signal(object) def worker(task_queue, result_queue): while True: data = task_queue.get() # 处理数据(示例:平方计算) result = data ** 2 result_queue.put(result) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.task_queue = Queue() self.result_queue = Queue() self.signals = ProcessSignals() # 启动子进程 self.proc = Process(target=worker, args=(self.task_queue, self.result_queue)) self.proc.start() # 监听结果队列 self.timer = QTimer() self.timer.timeout.connect(self.check_results) self.timer.start(100) # 每100ms检查一次结果 ``` #### 2. 进程间通信 ```python def check_results(self): while not self.result_queue.empty(): result = self.result_queue.get() self.signals.result_ready.emit(result) # 通过信号传递结果到GUI线程 self.label.setText(f"结果: {result}") ``` #### 注意事项 - 禁止在子进程中直接操作GUI组件[^1] - 复杂对象需使用$pickle$序列化 - 通过$Queue$或$Pipe$传递数据 --- ### 方法二:使用Qt的QProcess类(适合调用外部程序) #### 1. 启动外部进程 ```python from PySide6.QtCore import QProcess def start_external_process(self): self.process = QProcess(self) self.process.readyReadStandardOutput.connect(self.handle_output) # 设置执行参数 program = "python" args = ["-c", "print('来自子进程的输出')"] self.process.start(program, args) ``` #### 2. 处理进程输出 ```python def handle_output(self): data = self.process.readAllStandardOutput().data().decode() self.textEdit.append(f"输出: {data}") # 安全更新UI ``` --- ### ░ 关键对比 | 特性 | multiprocessing | QProcess | |--------------------|------------------------------|---------------------------| | 适用场景 | Python计算任务 | 调用外部程序/脚本 | | 通信方式 | Queue/Pipe | 标准输入/输出 | | GUI集成度 | 需手动信号转发 | 原生Qt事件循环集成 | | 跨进程对象传递 | 支持Python对象 | 仅文本/二进制数据 | --- ### ░ 最佳实践建议 1. **耗时操作分离**:计算耗时超过200ms的任务建议放入子进程 2. **异常处理**: ```python self.process.errorOccurred.connect(self.handle_error) # QProcess错误监控 ``` 3. **资源释放**: ```python def closeEvent(self, event): self.proc.terminate() # 终止multiprocessing进程 self.process.kill() # 终止QProcess ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

文子阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值