通过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()

三、效果图