2025-12-03 07:42:43 +00:00
|
|
|
|
import sys
|
|
|
|
|
|
import os
|
|
|
|
|
|
import ast
|
|
|
|
|
|
import json
|
|
|
|
|
|
import re
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout,
|
|
|
|
|
|
QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton,
|
|
|
|
|
|
QTextEdit, QScrollArea, QMessageBox, QFrame, QSpinBox,
|
|
|
|
|
|
QDoubleSpinBox, QTableWidget, QTableWidgetItem, QHeaderView,
|
|
|
|
|
|
QToolBar, QStatusBar, QDialog, QDialogButtonBox,
|
|
|
|
|
|
QGroupBox, QSplitter, QListWidget, QComboBox, QInputDialog,
|
|
|
|
|
|
QFileDialog, QMenuBar, QMenu)
|
|
|
|
|
|
from PyQt6.QtCore import Qt, QSize, QTimer
|
|
|
|
|
|
from PyQt6.QtGui import QAction
|
|
|
|
|
|
|
|
|
|
|
|
class ConfigSettingsDialog(QDialog):
|
|
|
|
|
|
"""配置文件设置对话框"""
|
|
|
|
|
|
def __init__(self, current_path="", parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.setWindowTitle("配置文件设置")
|
|
|
|
|
|
self.setModal(True)
|
|
|
|
|
|
self.setMinimumSize(500, 200)
|
|
|
|
|
|
self.current_path = current_path
|
|
|
|
|
|
|
|
|
|
|
|
self.init_ui()
|
|
|
|
|
|
|
|
|
|
|
|
def init_ui(self):
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setSpacing(12)
|
|
|
|
|
|
|
|
|
|
|
|
# 文件路径设置
|
|
|
|
|
|
file_group = QGroupBox("配置文件路径")
|
|
|
|
|
|
file_layout = QVBoxLayout(file_group)
|
|
|
|
|
|
|
|
|
|
|
|
path_layout = QHBoxLayout()
|
|
|
|
|
|
path_layout.addWidget(QLabel("配置文件:"))
|
|
|
|
|
|
|
|
|
|
|
|
self.path_edit = QLineEdit()
|
|
|
|
|
|
self.path_edit.setText(self.current_path)
|
|
|
|
|
|
path_layout.addWidget(self.path_edit)
|
|
|
|
|
|
|
|
|
|
|
|
self.browse_btn = QPushButton("浏览...")
|
|
|
|
|
|
self.browse_btn.clicked.connect(self.browse_file)
|
|
|
|
|
|
path_layout.addWidget(self.browse_btn)
|
|
|
|
|
|
|
|
|
|
|
|
file_layout.addLayout(path_layout)
|
|
|
|
|
|
|
|
|
|
|
|
# 提示信息
|
|
|
|
|
|
tip_label = QLabel("提示:请选择或输入要编辑的配置文件路径")
|
|
|
|
|
|
tip_label.setStyleSheet("color: #666666; font-size: 11px;")
|
|
|
|
|
|
file_layout.addWidget(tip_label)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(file_group)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用相对路径选项
|
|
|
|
|
|
self.use_relative_checkbox = QCheckBox("使用相对路径(相对于程序所在目录)")
|
|
|
|
|
|
self.use_relative_checkbox.setChecked(True)
|
|
|
|
|
|
layout.addWidget(self.use_relative_checkbox)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
# 按钮
|
|
|
|
|
|
button_box = QDialogButtonBox(
|
|
|
|
|
|
QDialogButtonBox.StandardButton.Ok |
|
|
|
|
|
|
QDialogButtonBox.StandardButton.Cancel
|
|
|
|
|
|
)
|
|
|
|
|
|
button_box.accepted.connect(self.accept)
|
|
|
|
|
|
button_box.rejected.connect(self.reject)
|
|
|
|
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
|
|
|
|
|
|
def browse_file(self):
|
|
|
|
|
|
"""浏览文件"""
|
|
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
|
|
|
|
self, "选择配置文件",
|
|
|
|
|
|
self.path_edit.text() or str(Path.home()),
|
|
|
|
|
|
"Python Files (*.py);;All Files (*)"
|
|
|
|
|
|
)
|
|
|
|
|
|
if file_path:
|
|
|
|
|
|
self.path_edit.setText(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
def get_settings(self):
|
|
|
|
|
|
"""获取设置"""
|
|
|
|
|
|
config_path = self.path_edit.text().strip()
|
|
|
|
|
|
|
|
|
|
|
|
# 如果使用相对路径,则转换为相对于程序目录的路径
|
|
|
|
|
|
if self.use_relative_checkbox.isChecked() and config_path:
|
|
|
|
|
|
if not os.path.isabs(config_path):
|
|
|
|
|
|
# 如果已经是相对路径,相对于程序所在目录
|
|
|
|
|
|
program_dir = Path(__file__).parent.absolute()
|
|
|
|
|
|
config_path = str(program_dir / config_path)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"config_file_path": config_path,
|
|
|
|
|
|
"use_relative_path": self.use_relative_checkbox.isChecked()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ConfigManagementWindow(QDialog):
|
|
|
|
|
|
def __init__(self, rules, config_fields, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.rules = rules
|
|
|
|
|
|
self.config_fields = config_fields
|
|
|
|
|
|
self.all_fields = list(config_fields.keys()) # 保存所有字段名用于搜索
|
|
|
|
|
|
self.setWindowTitle("规则管理")
|
|
|
|
|
|
self.setModal(True)
|
|
|
|
|
|
self.setMinimumSize(700, 600)
|
|
|
|
|
|
self.resize(700, 600)
|
|
|
|
|
|
|
|
|
|
|
|
self.init_ui()
|
|
|
|
|
|
self.load_data()
|
|
|
|
|
|
|
|
|
|
|
|
def init_ui(self):
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
layout.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
# 分割窗口
|
|
|
|
|
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
|
|
layout.addWidget(splitter)
|
|
|
|
|
|
|
|
|
|
|
|
# 左侧:配置项列表
|
|
|
|
|
|
left_widget = QWidget()
|
|
|
|
|
|
left_layout = QVBoxLayout(left_widget)
|
|
|
|
|
|
left_layout.setContentsMargins(4, 4, 4, 4)
|
|
|
|
|
|
|
|
|
|
|
|
# 搜索框
|
|
|
|
|
|
search_layout = QHBoxLayout()
|
|
|
|
|
|
search_layout.addWidget(QLabel("搜索:"))
|
|
|
|
|
|
self.search_edit = QLineEdit()
|
|
|
|
|
|
self.search_edit.setPlaceholderText("输入配置项名称进行搜索...")
|
|
|
|
|
|
self.search_edit.textChanged.connect(self.filter_fields)
|
|
|
|
|
|
search_layout.addWidget(self.search_edit)
|
|
|
|
|
|
|
|
|
|
|
|
# 清除搜索按钮
|
|
|
|
|
|
self.clear_search_btn = QPushButton("清除")
|
|
|
|
|
|
self.clear_search_btn.clicked.connect(self.clear_search)
|
|
|
|
|
|
self.clear_search_btn.setMaximumWidth(60)
|
|
|
|
|
|
search_layout.addWidget(self.clear_search_btn)
|
|
|
|
|
|
|
|
|
|
|
|
left_layout.addLayout(search_layout)
|
|
|
|
|
|
|
|
|
|
|
|
left_layout.addWidget(QLabel("配置项列表:"))
|
|
|
|
|
|
self.fields_list = QListWidget()
|
|
|
|
|
|
self.fields_list.currentItemChanged.connect(self.on_field_selected)
|
|
|
|
|
|
left_layout.addWidget(self.fields_list)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加统计信息
|
|
|
|
|
|
self.fields_count_label = QLabel("")
|
|
|
|
|
|
left_layout.addWidget(self.fields_count_label)
|
|
|
|
|
|
|
|
|
|
|
|
splitter.addWidget(left_widget)
|
|
|
|
|
|
|
|
|
|
|
|
# 右侧:编辑区域
|
|
|
|
|
|
right_widget = QWidget()
|
|
|
|
|
|
right_layout = QVBoxLayout(right_widget)
|
|
|
|
|
|
right_layout.setContentsMargins(4, 4, 4, 4)
|
|
|
|
|
|
|
|
|
|
|
|
# 基本信息
|
|
|
|
|
|
info_group = QGroupBox("字段属性")
|
|
|
|
|
|
form_layout = QVBoxLayout(info_group)
|
|
|
|
|
|
form_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
|
|
|
|
|
|
# 变量名
|
|
|
|
|
|
self.name_label = QLabel()
|
|
|
|
|
|
self.create_form_row(form_layout, "变量名:", self.name_label)
|
|
|
|
|
|
|
|
|
|
|
|
# 显示名称
|
|
|
|
|
|
self.display_name_edit = QLineEdit()
|
|
|
|
|
|
self.create_form_row(form_layout, "显示名称:", self.display_name_edit)
|
|
|
|
|
|
|
|
|
|
|
|
# 分组和类型
|
|
|
|
|
|
category_layout = QHBoxLayout()
|
|
|
|
|
|
category_layout.addWidget(QLabel("分组:"))
|
|
|
|
|
|
self.category_combo = QComboBox()
|
|
|
|
|
|
self.category_combo.setEditable(True)
|
|
|
|
|
|
category_layout.addWidget(self.category_combo)
|
|
|
|
|
|
category_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
category_layout.addWidget(QLabel("类型:"))
|
|
|
|
|
|
self.type_combo = QComboBox()
|
|
|
|
|
|
self.type_combo.addItems(["auto", "str", "int", "float", "bool", "json"])
|
|
|
|
|
|
self.type_combo.currentTextChanged.connect(self.on_type_changed)
|
|
|
|
|
|
category_layout.addWidget(self.type_combo)
|
|
|
|
|
|
|
|
|
|
|
|
form_layout.addLayout(category_layout)
|
|
|
|
|
|
|
|
|
|
|
|
# 小数位数设置(仅当类型为float或int时显示)
|
|
|
|
|
|
self.decimal_row = QHBoxLayout()
|
|
|
|
|
|
self.decimal_label = QLabel("小数位数:")
|
|
|
|
|
|
self.decimal_spinbox = QSpinBox()
|
|
|
|
|
|
self.decimal_spinbox.setRange(0, 10)
|
|
|
|
|
|
self.decimal_spinbox.setValue(2) # 默认值
|
|
|
|
|
|
self.decimal_spinbox.setEnabled(False) # 默认禁用,只有float类型才启用
|
|
|
|
|
|
self.decimal_row.addWidget(self.decimal_label)
|
|
|
|
|
|
self.decimal_row.addWidget(self.decimal_spinbox)
|
|
|
|
|
|
self.decimal_row.addStretch()
|
|
|
|
|
|
form_layout.addLayout(self.decimal_row)
|
|
|
|
|
|
|
|
|
|
|
|
# 隐藏状态
|
|
|
|
|
|
self.hidden_checkbox = QCheckBox("隐藏此配置项(不在主界面显示)")
|
|
|
|
|
|
form_layout.addWidget(self.hidden_checkbox)
|
|
|
|
|
|
|
|
|
|
|
|
# 提示信息
|
|
|
|
|
|
form_layout.addWidget(QLabel("提示信息:"))
|
|
|
|
|
|
self.tooltip_edit = QTextEdit()
|
|
|
|
|
|
self.tooltip_edit.setMaximumHeight(80)
|
|
|
|
|
|
form_layout.addWidget(self.tooltip_edit)
|
|
|
|
|
|
|
|
|
|
|
|
right_layout.addWidget(info_group)
|
|
|
|
|
|
|
|
|
|
|
|
# 校验规则
|
|
|
|
|
|
validation_group = QGroupBox("校验规则")
|
|
|
|
|
|
validation_layout = QVBoxLayout(validation_group)
|
|
|
|
|
|
validation_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
|
|
|
|
|
|
# 最小值
|
|
|
|
|
|
min_layout = QHBoxLayout()
|
|
|
|
|
|
min_layout.addWidget(QLabel("最小值:"))
|
|
|
|
|
|
self.min_edit = QLineEdit()
|
|
|
|
|
|
self.min_edit.setPlaceholderText("对于数字类型有效")
|
|
|
|
|
|
min_layout.addWidget(self.min_edit)
|
|
|
|
|
|
min_layout.addStretch()
|
|
|
|
|
|
validation_layout.addLayout(min_layout)
|
|
|
|
|
|
|
|
|
|
|
|
# 最大值
|
|
|
|
|
|
max_layout = QHBoxLayout()
|
|
|
|
|
|
max_layout.addWidget(QLabel("最大值:"))
|
|
|
|
|
|
self.max_edit = QLineEdit()
|
|
|
|
|
|
self.max_edit.setPlaceholderText("对于数字类型有效")
|
|
|
|
|
|
max_layout.addWidget(self.max_edit)
|
|
|
|
|
|
max_layout.addStretch()
|
|
|
|
|
|
validation_layout.addLayout(max_layout)
|
|
|
|
|
|
|
|
|
|
|
|
# 正则表达式
|
|
|
|
|
|
validation_layout.addWidget(QLabel("正则表达式:"))
|
|
|
|
|
|
self.regex_edit = QLineEdit()
|
|
|
|
|
|
self.regex_edit.setPlaceholderText("对于字符串类型有效,如: ^[A-Za-z0-9_]+$")
|
|
|
|
|
|
validation_layout.addWidget(self.regex_edit)
|
|
|
|
|
|
|
|
|
|
|
|
# 必填项
|
|
|
|
|
|
self.required_checkbox = QCheckBox("必填项")
|
|
|
|
|
|
validation_layout.addWidget(self.required_checkbox)
|
|
|
|
|
|
|
|
|
|
|
|
right_layout.addWidget(validation_group)
|
|
|
|
|
|
|
|
|
|
|
|
# 分组管理
|
|
|
|
|
|
group_group = QGroupBox("分组管理")
|
|
|
|
|
|
group_layout = QVBoxLayout(group_group)
|
|
|
|
|
|
|
|
|
|
|
|
group_btn_layout = QHBoxLayout()
|
|
|
|
|
|
self.add_group_btn = QPushButton("添加分组")
|
|
|
|
|
|
self.remove_group_btn = QPushButton("删除分组")
|
|
|
|
|
|
|
|
|
|
|
|
self.add_group_btn.clicked.connect(self.add_group)
|
|
|
|
|
|
self.remove_group_btn.clicked.connect(self.remove_group)
|
|
|
|
|
|
|
|
|
|
|
|
group_btn_layout.addWidget(self.add_group_btn)
|
|
|
|
|
|
group_btn_layout.addWidget(self.remove_group_btn)
|
|
|
|
|
|
group_btn_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
group_layout.addLayout(group_btn_layout)
|
|
|
|
|
|
right_layout.addWidget(group_group)
|
|
|
|
|
|
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 批量操作
|
|
|
|
|
|
batch_group = QGroupBox("批量操作")
|
|
|
|
|
|
batch_layout = QVBoxLayout(batch_group)
|
|
|
|
|
|
batch_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
|
|
|
|
|
|
batch_btn_layout = QHBoxLayout()
|
|
|
|
|
|
self.show_all_btn = QPushButton("全部显示")
|
|
|
|
|
|
self.show_all_btn.clicked.connect(self.show_all_fields)
|
|
|
|
|
|
self.hide_all_btn = QPushButton("全部隐藏")
|
|
|
|
|
|
self.hide_all_btn.clicked.connect(self.hide_all_fields)
|
|
|
|
|
|
|
|
|
|
|
|
batch_btn_layout.addWidget(self.show_all_btn)
|
|
|
|
|
|
batch_btn_layout.addWidget(self.hide_all_btn)
|
|
|
|
|
|
batch_btn_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
batch_layout.addLayout(batch_btn_layout)
|
|
|
|
|
|
right_layout.addWidget(batch_group)
|
|
|
|
|
|
|
2025-12-03 07:42:43 +00:00
|
|
|
|
right_layout.addStretch()
|
|
|
|
|
|
splitter.addWidget(right_widget)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置分割比例
|
|
|
|
|
|
splitter.setSizes([250, 420])
|
|
|
|
|
|
|
|
|
|
|
|
# 按钮
|
|
|
|
|
|
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
|
|
|
|
QDialogButtonBox.StandardButton.Cancel)
|
|
|
|
|
|
button_box.accepted.connect(self.accept)
|
|
|
|
|
|
button_box.rejected.connect(self.reject)
|
|
|
|
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
|
|
|
|
|
|
def create_form_row(self, layout, label_text, widget):
|
|
|
|
|
|
"""创建舒适的表单行"""
|
|
|
|
|
|
row_layout = QHBoxLayout()
|
|
|
|
|
|
label = QLabel(label_text)
|
|
|
|
|
|
label.setMinimumWidth(80)
|
|
|
|
|
|
row_layout.addWidget(label)
|
|
|
|
|
|
row_layout.addWidget(widget)
|
|
|
|
|
|
layout.addLayout(row_layout)
|
|
|
|
|
|
|
|
|
|
|
|
def load_data(self):
|
|
|
|
|
|
"""加载数据到界面"""
|
|
|
|
|
|
self.fields_list.clear()
|
|
|
|
|
|
for field_name in sorted(self.config_fields.keys()):
|
|
|
|
|
|
self.fields_list.addItem(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
self.update_fields_count()
|
|
|
|
|
|
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 加载分组
|
2025-12-03 07:42:43 +00:00
|
|
|
|
self.category_combo.clear()
|
|
|
|
|
|
categories = list(self.rules.get("categories", {}).keys())
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 确保"未分类"分组始终存在
|
|
|
|
|
|
if "未分类" not in categories:
|
|
|
|
|
|
categories.append("未分类")
|
|
|
|
|
|
if categories:
|
|
|
|
|
|
for category in sorted(categories):
|
|
|
|
|
|
self.category_combo.addItem(category)
|
2025-12-03 07:42:43 +00:00
|
|
|
|
|
|
|
|
|
|
def filter_fields(self, search_text):
|
|
|
|
|
|
"""根据搜索文本过滤配置项列表"""
|
|
|
|
|
|
search_text = search_text.strip().lower()
|
|
|
|
|
|
|
|
|
|
|
|
if not search_text:
|
|
|
|
|
|
# 如果搜索文本为空,显示所有项
|
|
|
|
|
|
for i in range(self.fields_list.count()):
|
|
|
|
|
|
item = self.fields_list.item(i)
|
|
|
|
|
|
item.setHidden(False)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 模糊匹配:显示包含搜索文本的项
|
|
|
|
|
|
for i in range(self.fields_list.count()):
|
|
|
|
|
|
item = self.fields_list.item(i)
|
|
|
|
|
|
field_name = item.text().lower()
|
|
|
|
|
|
if search_text in field_name:
|
|
|
|
|
|
item.setHidden(False)
|
|
|
|
|
|
else:
|
|
|
|
|
|
item.setHidden(True)
|
|
|
|
|
|
|
|
|
|
|
|
self.update_fields_count()
|
|
|
|
|
|
|
|
|
|
|
|
def clear_search(self):
|
|
|
|
|
|
"""清除搜索框内容"""
|
|
|
|
|
|
self.search_edit.clear()
|
|
|
|
|
|
|
|
|
|
|
|
def update_fields_count(self):
|
|
|
|
|
|
"""更新配置项计数显示"""
|
|
|
|
|
|
total_count = self.fields_list.count()
|
|
|
|
|
|
visible_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(total_count):
|
|
|
|
|
|
if not self.fields_list.item(i).isHidden():
|
|
|
|
|
|
visible_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
if visible_count == total_count:
|
|
|
|
|
|
self.fields_count_label.setText(f"总计: {total_count} 个配置项")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.fields_count_label.setText(f"显示: {visible_count}/{total_count} 个配置项")
|
|
|
|
|
|
|
|
|
|
|
|
def on_type_changed(self, field_type):
|
|
|
|
|
|
"""当类型改变时,控制小数位数设置的显示"""
|
|
|
|
|
|
# 只有float类型才显示小数位数设置
|
|
|
|
|
|
if field_type == "float":
|
|
|
|
|
|
self.decimal_label.setVisible(True)
|
|
|
|
|
|
self.decimal_spinbox.setVisible(True)
|
|
|
|
|
|
self.decimal_spinbox.setEnabled(True)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.decimal_label.setVisible(False)
|
|
|
|
|
|
self.decimal_spinbox.setVisible(False)
|
|
|
|
|
|
self.decimal_spinbox.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
|
|
def on_field_selected(self, current, previous):
|
|
|
|
|
|
# 保存前一个字段的更改
|
|
|
|
|
|
if previous is not None:
|
|
|
|
|
|
previous_field_name = previous.text()
|
|
|
|
|
|
self.save_current_field_changes(previous_field_name)
|
|
|
|
|
|
|
|
|
|
|
|
if not current:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
field_name = current.text()
|
|
|
|
|
|
self.name_label.setText(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
display_name = self.rules.get("display_names", {}).get(field_name, field_name)
|
|
|
|
|
|
self.display_name_edit.setText(display_name)
|
|
|
|
|
|
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 查找配置项所属的分组
|
2025-12-03 07:42:43 +00:00
|
|
|
|
category = "未分类"
|
|
|
|
|
|
for cat, fields in self.rules.get("categories", {}).items():
|
|
|
|
|
|
if field_name in fields:
|
|
|
|
|
|
category = cat
|
|
|
|
|
|
break
|
|
|
|
|
|
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 设置分组
|
2025-12-03 07:42:43 +00:00
|
|
|
|
index = self.category_combo.findText(category)
|
|
|
|
|
|
if index >= 0:
|
|
|
|
|
|
self.category_combo.setCurrentIndex(index)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.category_combo.setCurrentText(category)
|
|
|
|
|
|
|
|
|
|
|
|
field_type = self.rules.get("field_types", {}).get(field_name, "auto")
|
|
|
|
|
|
index = self.type_combo.findText(field_type)
|
|
|
|
|
|
if index >= 0:
|
|
|
|
|
|
self.type_combo.setCurrentIndex(index)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置小数位数
|
|
|
|
|
|
decimals = self.rules.get("field_decimals", {}).get(field_name, 6) # 默认6位小数
|
|
|
|
|
|
self.decimal_spinbox.setValue(decimals)
|
|
|
|
|
|
|
|
|
|
|
|
# 根据类型显示/隐藏小数位数设置
|
|
|
|
|
|
self.on_type_changed(field_type)
|
|
|
|
|
|
|
|
|
|
|
|
# 隐藏状态
|
|
|
|
|
|
hidden = field_name in self.rules.get("hidden", [])
|
|
|
|
|
|
self.hidden_checkbox.setChecked(hidden)
|
|
|
|
|
|
|
|
|
|
|
|
tooltip = self.rules.get("tooltips", {}).get(field_name, "")
|
|
|
|
|
|
self.tooltip_edit.setPlainText(tooltip)
|
|
|
|
|
|
|
|
|
|
|
|
# 校验规则
|
|
|
|
|
|
validation = self.rules.get("validations", {}).get(field_name, {})
|
|
|
|
|
|
self.min_edit.setText(validation.get("min", ""))
|
|
|
|
|
|
self.max_edit.setText(validation.get("max", ""))
|
|
|
|
|
|
self.regex_edit.setText(validation.get("regex", ""))
|
|
|
|
|
|
self.required_checkbox.setChecked(validation.get("required", False))
|
|
|
|
|
|
|
|
|
|
|
|
def save_current_field_changes(self, field_name):
|
|
|
|
|
|
"""保存当前字段的更改到规则字典"""
|
|
|
|
|
|
if not field_name:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 更新显示名称
|
|
|
|
|
|
display_name = self.display_name_edit.text().strip()
|
|
|
|
|
|
if display_name:
|
|
|
|
|
|
self.rules.setdefault("display_names", {})[field_name] = display_name
|
|
|
|
|
|
elif field_name in self.rules.get("display_names", {}):
|
|
|
|
|
|
del self.rules["display_names"][field_name]
|
|
|
|
|
|
|
|
|
|
|
|
# 更新分组
|
|
|
|
|
|
category = self.category_combo.currentText().strip()
|
2025-12-12 09:49:12 +00:00
|
|
|
|
if category == "未分类":
|
|
|
|
|
|
# 从所有分组中移除该字段
|
|
|
|
|
|
for cat, fields in self.rules.setdefault("categories", {}).items():
|
|
|
|
|
|
if cat != "未分类" and field_name in fields:
|
|
|
|
|
|
fields.remove(field_name)
|
|
|
|
|
|
# 确保在"未分类"分组中
|
|
|
|
|
|
if "未分类" not in self.rules["categories"]:
|
|
|
|
|
|
self.rules["categories"]["未分类"] = []
|
|
|
|
|
|
if field_name not in self.rules["categories"]["未分类"]:
|
|
|
|
|
|
self.rules["categories"]["未分类"].append(field_name)
|
|
|
|
|
|
elif category: # 其他分组
|
|
|
|
|
|
# 从所有分组中移除该字段
|
|
|
|
|
|
for cat, fields in self.rules.setdefault("categories", {}).items():
|
2025-12-03 07:42:43 +00:00
|
|
|
|
if field_name in fields:
|
|
|
|
|
|
fields.remove(field_name)
|
2025-12-12 09:49:12 +00:00
|
|
|
|
|
|
|
|
|
|
# 确保"未分类"分组存在
|
|
|
|
|
|
if "未分类" not in self.rules["categories"]:
|
|
|
|
|
|
self.rules["categories"]["未分类"] = []
|
2025-12-03 07:42:43 +00:00
|
|
|
|
|
|
|
|
|
|
# 添加到新分组
|
|
|
|
|
|
if category not in self.rules["categories"]:
|
|
|
|
|
|
self.rules["categories"][category] = []
|
|
|
|
|
|
if field_name not in self.rules["categories"][category]:
|
|
|
|
|
|
self.rules["categories"][category].append(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
# 更新字段类型
|
|
|
|
|
|
field_type = self.type_combo.currentText()
|
|
|
|
|
|
if field_type != "auto":
|
|
|
|
|
|
self.rules.setdefault("field_types", {})[field_name] = field_type
|
|
|
|
|
|
|
|
|
|
|
|
# 更新小数位数(仅当类型为float时保存)
|
|
|
|
|
|
if field_type == "float":
|
|
|
|
|
|
decimals = self.decimal_spinbox.value()
|
|
|
|
|
|
self.rules.setdefault("field_decimals", {})[field_name] = decimals
|
|
|
|
|
|
elif field_name in self.rules.get("field_decimals", {}):
|
|
|
|
|
|
# 如果类型不是float但存在小数位数设置,删除它
|
|
|
|
|
|
del self.rules["field_decimals"][field_name]
|
|
|
|
|
|
elif field_name in self.rules.get("field_types", {}):
|
|
|
|
|
|
del self.rules["field_types"][field_name]
|
|
|
|
|
|
# 如果字段类型被删除,也删除小数位数设置
|
|
|
|
|
|
if field_name in self.rules.get("field_decimals", {}):
|
|
|
|
|
|
del self.rules["field_decimals"][field_name]
|
|
|
|
|
|
|
|
|
|
|
|
# 更新提示信息
|
|
|
|
|
|
tooltip = self.tooltip_edit.toPlainText().strip()
|
|
|
|
|
|
if tooltip:
|
|
|
|
|
|
self.rules.setdefault("tooltips", {})[field_name] = tooltip
|
|
|
|
|
|
elif field_name in self.rules.get("tooltips", {}):
|
|
|
|
|
|
del self.rules["tooltips"][field_name]
|
|
|
|
|
|
|
|
|
|
|
|
# 更新隐藏状态
|
|
|
|
|
|
hidden = self.hidden_checkbox.isChecked()
|
|
|
|
|
|
hidden_list = self.rules.setdefault("hidden", [])
|
|
|
|
|
|
if hidden and field_name not in hidden_list:
|
|
|
|
|
|
hidden_list.append(field_name)
|
|
|
|
|
|
elif not hidden and field_name in hidden_list:
|
|
|
|
|
|
hidden_list.remove(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
# 更新校验规则
|
|
|
|
|
|
validation = {}
|
|
|
|
|
|
min_val = self.min_edit.text().strip()
|
|
|
|
|
|
if min_val:
|
|
|
|
|
|
validation["min"] = min_val
|
|
|
|
|
|
|
|
|
|
|
|
max_val = self.max_edit.text().strip()
|
|
|
|
|
|
if max_val:
|
|
|
|
|
|
validation["max"] = max_val
|
|
|
|
|
|
|
|
|
|
|
|
regex_val = self.regex_edit.text().strip()
|
|
|
|
|
|
if regex_val:
|
|
|
|
|
|
validation["regex"] = regex_val
|
|
|
|
|
|
|
|
|
|
|
|
validation["required"] = self.required_checkbox.isChecked()
|
|
|
|
|
|
|
|
|
|
|
|
if validation:
|
|
|
|
|
|
self.rules.setdefault("validations", {})[field_name] = validation
|
|
|
|
|
|
elif field_name in self.rules.get("validations", {}):
|
|
|
|
|
|
del self.rules["validations"][field_name]
|
|
|
|
|
|
|
|
|
|
|
|
def accept(self):
|
|
|
|
|
|
"""重写accept方法,确保保存当前字段的更改"""
|
|
|
|
|
|
current_item = self.fields_list.currentItem()
|
|
|
|
|
|
if current_item:
|
|
|
|
|
|
field_name = current_item.text()
|
|
|
|
|
|
self.save_current_field_changes(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
super().accept()
|
|
|
|
|
|
|
|
|
|
|
|
def add_group(self):
|
|
|
|
|
|
new_group, ok = QInputDialog.getText(self, "添加分组", "请输入新分组名称:")
|
|
|
|
|
|
if ok and new_group.strip():
|
|
|
|
|
|
if new_group not in self.rules.setdefault("categories", {}):
|
|
|
|
|
|
self.rules["categories"][new_group] = []
|
|
|
|
|
|
self.category_combo.addItem(new_group)
|
|
|
|
|
|
|
|
|
|
|
|
def remove_group(self):
|
|
|
|
|
|
current_group = self.category_combo.currentText()
|
|
|
|
|
|
if current_group and current_group != "未分类":
|
|
|
|
|
|
reply = QMessageBox.question(self, "确认删除",
|
2025-12-12 09:49:12 +00:00
|
|
|
|
f"确定要删除分组 '{current_group}' 吗?\n该分组中的所有配置项将被移到'未分类'分组。")
|
2025-12-03 07:42:43 +00:00
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
if current_group in self.rules.get("categories", {}):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 将该分组中的配置项移到"未分类"分组
|
|
|
|
|
|
fields_to_move = self.rules["categories"][current_group]
|
|
|
|
|
|
if "未分类" not in self.rules["categories"]:
|
|
|
|
|
|
self.rules["categories"]["未分类"] = []
|
|
|
|
|
|
|
|
|
|
|
|
for field_name in fields_to_move:
|
|
|
|
|
|
if field_name not in self.rules["categories"]["未分类"]:
|
|
|
|
|
|
self.rules["categories"]["未分类"].append(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
# 删除分组
|
2025-12-03 07:42:43 +00:00
|
|
|
|
del self.rules["categories"][current_group]
|
2025-12-12 09:49:12 +00:00
|
|
|
|
|
|
|
|
|
|
# 从下拉框中移除
|
|
|
|
|
|
index = self.category_combo.findText(current_group)
|
|
|
|
|
|
if index >= 0:
|
|
|
|
|
|
self.category_combo.removeItem(index)
|
|
|
|
|
|
# 设置当前选中的分组为"未分类"
|
|
|
|
|
|
self.category_combo.setCurrentText("未分类")
|
|
|
|
|
|
|
|
|
|
|
|
def show_all_fields(self):
|
|
|
|
|
|
"""批量显示所有配置项(取消所有隐藏)"""
|
|
|
|
|
|
reply = QMessageBox.question(self, "确认",
|
|
|
|
|
|
"确定要取消所有配置项的隐藏状态吗?",
|
|
|
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
# 清空隐藏列表
|
|
|
|
|
|
self.rules["hidden"] = []
|
|
|
|
|
|
|
|
|
|
|
|
# 更新当前选中字段的隐藏状态
|
|
|
|
|
|
current_item = self.fields_list.currentItem()
|
|
|
|
|
|
if current_item:
|
|
|
|
|
|
self.hidden_checkbox.setChecked(False)
|
|
|
|
|
|
|
|
|
|
|
|
QMessageBox.information(self, "完成", "已取消所有配置项的隐藏状态")
|
|
|
|
|
|
|
|
|
|
|
|
def hide_all_fields(self):
|
|
|
|
|
|
"""批量隐藏所有配置项"""
|
|
|
|
|
|
reply = QMessageBox.question(self, "确认",
|
|
|
|
|
|
"确定要隐藏所有配置项吗?",
|
|
|
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
# 获取所有配置项的名称
|
|
|
|
|
|
all_fields = list(self.config_fields.keys())
|
|
|
|
|
|
|
|
|
|
|
|
# 将所有配置项添加到隐藏列表
|
|
|
|
|
|
self.rules["hidden"] = all_fields
|
|
|
|
|
|
|
|
|
|
|
|
# 更新当前选中字段的隐藏状态
|
|
|
|
|
|
current_item = self.fields_list.currentItem()
|
|
|
|
|
|
if current_item:
|
|
|
|
|
|
self.hidden_checkbox.setChecked(True)
|
|
|
|
|
|
|
|
|
|
|
|
QMessageBox.information(self, "完成", "已隐藏所有配置项")
|
2025-12-03 07:42:43 +00:00
|
|
|
|
|
|
|
|
|
|
def get_updated_rules(self):
|
|
|
|
|
|
"""获取更新后的规则"""
|
|
|
|
|
|
return self.rules
|
|
|
|
|
|
|
|
|
|
|
|
class NoWheelSpinBox(QSpinBox):
|
|
|
|
|
|
"""禁用鼠标滚轮的SpinBox"""
|
|
|
|
|
|
def wheelEvent(self, event):
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
|
|
|
|
|
|
class NoWheelDoubleSpinBox(QDoubleSpinBox):
|
|
|
|
|
|
"""禁用鼠标滚轮的DoubleSpinBox"""
|
|
|
|
|
|
def wheelEvent(self, event):
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
|
|
|
|
|
|
def setDecimalsFromRules(self, decimals):
|
|
|
|
|
|
"""根据规则设置小数位数"""
|
|
|
|
|
|
self.setDecimals(decimals)
|
|
|
|
|
|
|
|
|
|
|
|
class ConfigField:
|
|
|
|
|
|
"""配置项元数据类"""
|
|
|
|
|
|
def __init__(self, name, value, category="未分类", display_name=None,
|
|
|
|
|
|
field_type="auto", decimals=None, tooltip="", hidden=False,
|
|
|
|
|
|
line_number=None, original_lines=None, validation=None):
|
|
|
|
|
|
self.name = name
|
|
|
|
|
|
self.value = value
|
|
|
|
|
|
self.category = category
|
|
|
|
|
|
self.display_name = display_name or name
|
|
|
|
|
|
self.field_type = field_type
|
|
|
|
|
|
self.decimals = decimals # 小数位数(仅对float类型有效)
|
|
|
|
|
|
self.tooltip = tooltip
|
|
|
|
|
|
self.hidden = hidden
|
|
|
|
|
|
self.line_number = line_number # 记录配置项在文件中的行号
|
|
|
|
|
|
self.original_lines = original_lines or [] # 保存原始行内容(包括注释)
|
|
|
|
|
|
self.validation = validation or {} # 校验规则
|
|
|
|
|
|
|
|
|
|
|
|
def get_actual_field_type(self):
|
|
|
|
|
|
"""获取实际的字段类型"""
|
|
|
|
|
|
if self.field_type != "auto":
|
|
|
|
|
|
return self.field_type
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(self.value, bool):
|
|
|
|
|
|
return "bool"
|
|
|
|
|
|
elif isinstance(self.value, int):
|
|
|
|
|
|
return "int"
|
|
|
|
|
|
elif isinstance(self.value, float):
|
|
|
|
|
|
return "float"
|
|
|
|
|
|
elif isinstance(self.value, (list, dict)):
|
|
|
|
|
|
return "json"
|
|
|
|
|
|
else:
|
|
|
|
|
|
return "str"
|
|
|
|
|
|
|
|
|
|
|
|
class ChangeConfirmDialog(QDialog):
|
|
|
|
|
|
"""变更确认对话框"""
|
|
|
|
|
|
def __init__(self, changes, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.setWindowTitle("确认配置变更")
|
|
|
|
|
|
self.setModal(True)
|
|
|
|
|
|
self.setMinimumSize(600, 400)
|
|
|
|
|
|
self.resize(600, 400)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置窗口标志,避免重影问题
|
|
|
|
|
|
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowTitleHint |
|
|
|
|
|
|
Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowStaysOnTopHint)
|
|
|
|
|
|
|
|
|
|
|
|
self.init_ui(changes)
|
|
|
|
|
|
|
|
|
|
|
|
def init_ui(self, changes):
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
|
|
|
|
|
|
# 标题
|
|
|
|
|
|
title_label = QLabel(f"以下 {len(changes)} 个配置项将被修改:")
|
|
|
|
|
|
title_label.setStyleSheet("font-weight: bold; font-size: 14px;")
|
|
|
|
|
|
layout.addWidget(title_label)
|
|
|
|
|
|
|
|
|
|
|
|
# 变更列表
|
|
|
|
|
|
table = QTableWidget()
|
|
|
|
|
|
table.setColumnCount(3)
|
|
|
|
|
|
table.setHorizontalHeaderLabels(["配置项", "原值", "新值"])
|
|
|
|
|
|
table.setRowCount(len(changes))
|
|
|
|
|
|
|
|
|
|
|
|
for row, (field_name, (old_value, new_value)) in enumerate(changes.items()):
|
|
|
|
|
|
table.setItem(row, 0, QTableWidgetItem(field_name))
|
|
|
|
|
|
table.setItem(row, 1, QTableWidgetItem(str(old_value)))
|
|
|
|
|
|
table.setItem(row, 2, QTableWidgetItem(str(new_value)))
|
|
|
|
|
|
|
|
|
|
|
|
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
|
|
|
|
|
table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
|
|
|
|
layout.addWidget(table)
|
|
|
|
|
|
|
|
|
|
|
|
# 按钮
|
|
|
|
|
|
button_box = QDialogButtonBox(
|
|
|
|
|
|
QDialogButtonBox.StandardButton.Ok |
|
|
|
|
|
|
QDialogButtonBox.StandardButton.Cancel
|
|
|
|
|
|
)
|
|
|
|
|
|
button_box.accepted.connect(self.accept)
|
|
|
|
|
|
button_box.rejected.connect(self.reject)
|
|
|
|
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
|
|
|
|
|
|
class ConfigEditor(QMainWindow):
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
|
|
|
|
# 获取程序所在目录
|
|
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
|
|
|
# 打包后的可执行文件
|
|
|
|
|
|
self.program_dir = Path(sys.executable).parent
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 直接运行脚本
|
|
|
|
|
|
self.program_dir = Path(__file__).parent
|
|
|
|
|
|
|
|
|
|
|
|
# 规则文件固定位置:与主程序在同一目录
|
|
|
|
|
|
self.rules_file = str(self.program_dir / "config_editor_rules.json")
|
|
|
|
|
|
|
|
|
|
|
|
# 用户设置文件
|
|
|
|
|
|
self.settings_file = str(self.program_dir / "config_editor_settings.json")
|
|
|
|
|
|
|
|
|
|
|
|
# 加载用户设置
|
|
|
|
|
|
self.user_settings = self.load_user_settings()
|
|
|
|
|
|
|
|
|
|
|
|
# 获取配置文件路径(如果有)
|
|
|
|
|
|
self.config_file_path = self.user_settings.get("config_file_path", "")
|
|
|
|
|
|
|
|
|
|
|
|
self.config_data = {}
|
|
|
|
|
|
self.original_config_data = {} # 保存原始配置数据用于比较
|
|
|
|
|
|
self.config_fields = {} # 过滤后的配置项(不包含隐藏的)
|
|
|
|
|
|
self.all_config_fields = {} # 所有配置项(包含隐藏的)
|
|
|
|
|
|
self.dynamic_widgets = {}
|
|
|
|
|
|
self.category_widgets = {}
|
|
|
|
|
|
self.original_content = ""
|
|
|
|
|
|
self.rules = {}
|
|
|
|
|
|
self.config_lines = [] # 存储每行内容和对应的配置项
|
|
|
|
|
|
self.modified_fields = set() # 记录被修改的字段
|
|
|
|
|
|
self.field_containers = {} # 存储字段的容器
|
|
|
|
|
|
|
|
|
|
|
|
self.init_ui()
|
|
|
|
|
|
self.load_rules()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否第一次启动(没有配置文件地址)
|
|
|
|
|
|
if not self.config_file_path or not os.path.exists(self.config_file_path):
|
|
|
|
|
|
# 首次启动,显示设置对话框
|
|
|
|
|
|
self.show_settings_dialog(initial_setup=True)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 非首次启动,加载配置文件
|
|
|
|
|
|
self.load_config()
|
|
|
|
|
|
|
|
|
|
|
|
def load_user_settings(self):
|
|
|
|
|
|
"""加载用户设置"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if os.path.exists(self.settings_file):
|
|
|
|
|
|
with open(self.settings_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"加载用户设置失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
def save_user_settings(self):
|
|
|
|
|
|
"""保存用户设置"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.user_settings.update({
|
|
|
|
|
|
"config_file_path": self.config_file_path,
|
|
|
|
|
|
"last_used": str(Path(self.config_file_path).absolute()) if self.config_file_path and os.path.exists(self.config_file_path) else ""
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
with open(self.settings_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
json.dump(self.user_settings, f, indent=2, ensure_ascii=False)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"保存用户设置失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def init_ui(self):
|
|
|
|
|
|
self.setWindowTitle("配置管理页")
|
|
|
|
|
|
self.setGeometry(100, 100, 1000, 700)
|
|
|
|
|
|
self.resize(1000, 700)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建菜单栏
|
|
|
|
|
|
menubar = self.menuBar()
|
|
|
|
|
|
|
|
|
|
|
|
# 文件菜单
|
|
|
|
|
|
file_menu = menubar.addMenu("文件")
|
|
|
|
|
|
|
|
|
|
|
|
self.open_action = QAction("打开配置文件", self)
|
|
|
|
|
|
self.open_action.triggered.connect(self.open_config_file)
|
|
|
|
|
|
file_menu.addAction(self.open_action)
|
|
|
|
|
|
|
|
|
|
|
|
self.settings_action = QAction("配置文件设置", self)
|
|
|
|
|
|
self.settings_action.triggered.connect(lambda: self.show_settings_dialog(initial_setup=False))
|
|
|
|
|
|
file_menu.addAction(self.settings_action)
|
|
|
|
|
|
|
|
|
|
|
|
file_menu.addSeparator()
|
|
|
|
|
|
|
|
|
|
|
|
self.exit_action = QAction("退出", self)
|
|
|
|
|
|
self.exit_action.triggered.connect(self.close)
|
|
|
|
|
|
file_menu.addAction(self.exit_action)
|
|
|
|
|
|
|
|
|
|
|
|
# 编辑菜单
|
|
|
|
|
|
edit_menu = menubar.addMenu("编辑")
|
|
|
|
|
|
|
|
|
|
|
|
self.reload_action = QAction("重新加载", self)
|
|
|
|
|
|
self.reload_action.triggered.connect(self.load_config)
|
|
|
|
|
|
edit_menu.addAction(self.reload_action)
|
|
|
|
|
|
|
|
|
|
|
|
self.save_action = QAction("保存配置", self)
|
|
|
|
|
|
self.save_action.triggered.connect(self.save_config)
|
|
|
|
|
|
edit_menu.addAction(self.save_action)
|
|
|
|
|
|
|
|
|
|
|
|
# 工具菜单
|
|
|
|
|
|
tools_menu = menubar.addMenu("工具")
|
|
|
|
|
|
|
|
|
|
|
|
self.rules_action = QAction("管理规则", self)
|
|
|
|
|
|
self.rules_action.triggered.connect(self.manage_rules)
|
|
|
|
|
|
tools_menu.addAction(self.rules_action)
|
|
|
|
|
|
|
|
|
|
|
|
# 状态栏
|
|
|
|
|
|
self.statusBar().showMessage("就绪")
|
|
|
|
|
|
|
|
|
|
|
|
central_widget = QWidget()
|
|
|
|
|
|
self.setCentralWidget(central_widget)
|
|
|
|
|
|
|
|
|
|
|
|
main_layout = QVBoxLayout(central_widget)
|
|
|
|
|
|
main_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
main_layout.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
# 顶部信息栏
|
|
|
|
|
|
info_frame = QFrame()
|
|
|
|
|
|
info_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
|
|
|
info_frame.setStyleSheet("""
|
|
|
|
|
|
QFrame {
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
info_layout = QHBoxLayout(info_frame)
|
|
|
|
|
|
info_layout.setContentsMargins(8, 4, 8, 4)
|
|
|
|
|
|
|
|
|
|
|
|
self.path_label = QLabel()
|
|
|
|
|
|
self.path_label.setStyleSheet("color: #666666; font-size: 11px;")
|
|
|
|
|
|
info_layout.addWidget(self.path_label)
|
|
|
|
|
|
info_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
main_layout.addWidget(info_frame)
|
|
|
|
|
|
|
|
|
|
|
|
self.tab_widget = QTabWidget()
|
|
|
|
|
|
main_layout.addWidget(self.tab_widget)
|
|
|
|
|
|
|
|
|
|
|
|
self.init_category_tabs()
|
|
|
|
|
|
|
|
|
|
|
|
# 按钮布局
|
|
|
|
|
|
button_layout = QHBoxLayout()
|
|
|
|
|
|
button_layout.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
self.open_btn = QPushButton("打开文件")
|
|
|
|
|
|
self.open_btn.clicked.connect(self.open_config_file)
|
|
|
|
|
|
button_layout.addWidget(self.open_btn)
|
|
|
|
|
|
|
|
|
|
|
|
self.settings_btn = QPushButton("配置文件设置")
|
|
|
|
|
|
self.settings_btn.clicked.connect(lambda: self.show_settings_dialog(initial_setup=False))
|
|
|
|
|
|
button_layout.addWidget(self.settings_btn)
|
|
|
|
|
|
|
|
|
|
|
|
button_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
self.save_btn = QPushButton("保存配置")
|
|
|
|
|
|
self.save_btn.clicked.connect(self.save_config)
|
|
|
|
|
|
self.save_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; }")
|
|
|
|
|
|
button_layout.addWidget(self.save_btn)
|
|
|
|
|
|
|
|
|
|
|
|
self.reload_btn = QPushButton("重新加载")
|
|
|
|
|
|
self.reload_btn.clicked.connect(self.load_config)
|
|
|
|
|
|
button_layout.addWidget(self.reload_btn)
|
|
|
|
|
|
|
|
|
|
|
|
self.rules_btn = QPushButton("管理规则")
|
|
|
|
|
|
self.rules_btn.clicked.connect(self.manage_rules)
|
|
|
|
|
|
button_layout.addWidget(self.rules_btn)
|
|
|
|
|
|
|
|
|
|
|
|
# 显示/隐藏配置项按钮
|
|
|
|
|
|
self.toggle_hidden_btn = QPushButton("显示隐藏项")
|
|
|
|
|
|
self.toggle_hidden_btn.setCheckable(True)
|
|
|
|
|
|
self.toggle_hidden_btn.toggled.connect(self.toggle_hidden_fields)
|
|
|
|
|
|
button_layout.addWidget(self.toggle_hidden_btn)
|
|
|
|
|
|
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 新增:一键隐藏所有配置项按钮
|
|
|
|
|
|
self.hide_all_btn = QPushButton("一键隐藏所有项")
|
|
|
|
|
|
self.hide_all_btn.clicked.connect(self.hide_all_fields)
|
|
|
|
|
|
self.hide_all_btn.setStyleSheet("QPushButton { background-color: #ff9800; color: white; }")
|
|
|
|
|
|
button_layout.addWidget(self.hide_all_btn)
|
|
|
|
|
|
|
2025-12-03 07:42:43 +00:00
|
|
|
|
main_layout.addLayout(button_layout)
|
|
|
|
|
|
|
|
|
|
|
|
# 更新路径显示
|
|
|
|
|
|
self.update_path_display()
|
|
|
|
|
|
|
|
|
|
|
|
def update_path_display(self):
|
|
|
|
|
|
"""更新路径显示"""
|
|
|
|
|
|
if self.config_file_path and os.path.exists(self.config_file_path):
|
|
|
|
|
|
abs_path = os.path.abspath(self.config_file_path)
|
|
|
|
|
|
self.path_label.setText(f"配置文件: {abs_path}")
|
|
|
|
|
|
self.path_label.setStyleSheet("color: #666666; font-size: 11px;")
|
|
|
|
|
|
elif self.config_file_path:
|
|
|
|
|
|
self.path_label.setText(f"配置文件不存在: {self.config_file_path} (点击'配置文件设置'调整)")
|
|
|
|
|
|
self.path_label.setStyleSheet("color: #ff0000; font-size: 11px;")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.path_label.setText("未设置配置文件路径 (点击'配置文件设置'配置)")
|
|
|
|
|
|
self.path_label.setStyleSheet("color: #ff9900; font-size: 11px;")
|
|
|
|
|
|
|
|
|
|
|
|
def check_config_file(self):
|
|
|
|
|
|
"""检查配置文件是否存在"""
|
|
|
|
|
|
if not self.config_file_path or not os.path.exists(self.config_file_path):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def show_settings_dialog(self, initial_setup=False):
|
|
|
|
|
|
"""显示设置对话框
|
|
|
|
|
|
initial_setup: 是否是首次启动的设置
|
|
|
|
|
|
"""
|
|
|
|
|
|
dialog = ConfigSettingsDialog(self.config_file_path, self)
|
|
|
|
|
|
|
|
|
|
|
|
if initial_setup:
|
|
|
|
|
|
# 首次启动,设置对话框模态且必须设置
|
|
|
|
|
|
dialog.setWindowTitle("首次启动 - 配置文件设置")
|
|
|
|
|
|
dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
|
|
|
|
|
|
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
|
|
|
|
settings = dialog.get_settings()
|
|
|
|
|
|
|
|
|
|
|
|
# 更新设置
|
|
|
|
|
|
self.config_file_path = settings["config_file_path"]
|
|
|
|
|
|
|
|
|
|
|
|
# 保存用户设置
|
|
|
|
|
|
self.save_user_settings()
|
|
|
|
|
|
|
|
|
|
|
|
# 更新路径显示
|
|
|
|
|
|
self.update_path_display()
|
|
|
|
|
|
|
|
|
|
|
|
# 重新加载配置
|
|
|
|
|
|
if self.check_config_file():
|
|
|
|
|
|
self.load_config()
|
|
|
|
|
|
if not initial_setup:
|
|
|
|
|
|
QMessageBox.information(self, "成功", "配置文件路径已更新并重新加载配置")
|
|
|
|
|
|
else:
|
|
|
|
|
|
QMessageBox.warning(self, "警告", "配置文件不存在,请检查路径")
|
|
|
|
|
|
elif initial_setup:
|
|
|
|
|
|
# 首次启动用户取消了设置,退出程序
|
|
|
|
|
|
QMessageBox.warning(self, "提示", "首次启动需要设置配置文件路径,程序将退出。")
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
def open_config_file(self):
|
|
|
|
|
|
"""打开配置文件"""
|
|
|
|
|
|
if self.modified_fields:
|
|
|
|
|
|
reply = QMessageBox.question(
|
|
|
|
|
|
self, "确认",
|
|
|
|
|
|
"当前有未保存的修改,是否保存?",
|
|
|
|
|
|
QMessageBox.StandardButton.Yes |
|
|
|
|
|
|
QMessageBox.StandardButton.No |
|
|
|
|
|
|
QMessageBox.StandardButton.Cancel
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if reply == QMessageBox.StandardButton.Cancel:
|
|
|
|
|
|
return
|
|
|
|
|
|
elif reply == QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
self.save_config()
|
|
|
|
|
|
|
|
|
|
|
|
# 使用文件对话框选择文件
|
|
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
|
|
|
|
self, "选择配置文件",
|
|
|
|
|
|
self.config_file_path or str(Path.home()),
|
|
|
|
|
|
"Python Files (*.py);;All Files (*)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if file_path:
|
|
|
|
|
|
self.config_file_path = file_path
|
|
|
|
|
|
self.save_user_settings()
|
|
|
|
|
|
self.update_path_display()
|
|
|
|
|
|
self.load_config()
|
|
|
|
|
|
|
|
|
|
|
|
def init_category_tabs(self):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""初始化分类标签页"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
self.category_widgets.clear()
|
|
|
|
|
|
categories = list(self.rules.get("categories", {}).keys())
|
|
|
|
|
|
if not categories:
|
|
|
|
|
|
categories = ["未分类"]
|
|
|
|
|
|
|
|
|
|
|
|
for category in categories:
|
|
|
|
|
|
self.create_category_tab(category)
|
|
|
|
|
|
|
|
|
|
|
|
def create_category_tab(self, category):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""创建分类标签页"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
tab = QWidget()
|
|
|
|
|
|
layout = QVBoxLayout(tab)
|
|
|
|
|
|
layout.setContentsMargins(4, 4, 4, 4)
|
|
|
|
|
|
|
|
|
|
|
|
scroll = QScrollArea()
|
|
|
|
|
|
scroll_widget = QWidget()
|
|
|
|
|
|
scroll_layout = QVBoxLayout(scroll_widget)
|
|
|
|
|
|
scroll_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
scroll_layout.setSpacing(6)
|
|
|
|
|
|
|
|
|
|
|
|
self.category_widgets[category] = scroll_layout
|
|
|
|
|
|
|
|
|
|
|
|
scroll.setWidget(scroll_widget)
|
|
|
|
|
|
scroll.setWidgetResizable(True)
|
|
|
|
|
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
|
|
|
|
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
|
|
|
|
layout.addWidget(scroll)
|
|
|
|
|
|
|
|
|
|
|
|
self.tab_widget.addTab(tab, category)
|
|
|
|
|
|
|
|
|
|
|
|
def load_rules(self):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""加载规则文件"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
try:
|
|
|
|
|
|
if os.path.exists(self.rules_file):
|
|
|
|
|
|
with open(self.rules_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
self.rules = json.load(f)
|
|
|
|
|
|
|
2025-12-12 09:49:12 +00:00
|
|
|
|
# 确保规则中有"未分类"分组
|
|
|
|
|
|
if "categories" not in self.rules:
|
|
|
|
|
|
self.rules["categories"] = {}
|
|
|
|
|
|
if "未分类" not in self.rules["categories"]:
|
|
|
|
|
|
self.rules["categories"]["未分类"] = []
|
|
|
|
|
|
|
2025-12-03 07:42:43 +00:00
|
|
|
|
# 确保规则中有field_decimals字段
|
|
|
|
|
|
if "field_decimals" not in self.rules:
|
|
|
|
|
|
self.rules["field_decimals"] = {}
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.rules = {
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"categories": {"未分类": []}, # 保留"未分类"分组
|
2025-12-03 07:42:43 +00:00
|
|
|
|
"display_names": {},
|
|
|
|
|
|
"tooltips": {},
|
|
|
|
|
|
"field_types": {},
|
|
|
|
|
|
"field_decimals": {}, # 新增:小数位数设置
|
|
|
|
|
|
"hidden": [], # 新增隐藏列表
|
|
|
|
|
|
"validations": {} # 新增校验规则
|
|
|
|
|
|
}
|
|
|
|
|
|
# 首次运行,尝试创建规则文件
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.save_rules()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"创建规则文件失败: {e}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"加载规则文件失败: {e}")
|
|
|
|
|
|
self.rules = {
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"categories": {"未分类": []},
|
2025-12-03 07:42:43 +00:00
|
|
|
|
"display_names": {},
|
|
|
|
|
|
"tooltips": {},
|
|
|
|
|
|
"field_types": {},
|
|
|
|
|
|
"field_decimals": {},
|
|
|
|
|
|
"hidden": [],
|
|
|
|
|
|
"validations": {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def save_rules(self):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""保存规则文件"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
try:
|
|
|
|
|
|
with open(self.rules_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
json.dump(self.rules, f, indent=2, ensure_ascii=False)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"保存规则文件失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def categorize_field(self, field_name):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""为字段分类,如果未分配分组则归到'未分类'"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
for category, fields in self.rules.get("categories", {}).items():
|
|
|
|
|
|
if field_name in fields:
|
|
|
|
|
|
return category
|
2025-12-12 09:49:12 +00:00
|
|
|
|
return "未分类" # 默认归到"未分类"
|
2025-12-03 07:42:43 +00:00
|
|
|
|
|
|
|
|
|
|
def get_display_name(self, field_name):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""获取字段的显示名称"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
return self.rules.get("display_names", {}).get(field_name, field_name)
|
|
|
|
|
|
|
|
|
|
|
|
def get_tooltip(self, field_name):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""获取字段的提示信息"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
return self.rules.get("tooltips", {}).get(field_name, f"配置项: {field_name}")
|
|
|
|
|
|
|
|
|
|
|
|
def get_field_type(self, field_name, value):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""获取字段类型"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
if field_name in self.rules.get("field_types", {}):
|
|
|
|
|
|
return self.rules["field_types"][field_name]
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(value, bool):
|
|
|
|
|
|
return "bool"
|
|
|
|
|
|
elif isinstance(value, int):
|
|
|
|
|
|
return "int"
|
|
|
|
|
|
elif isinstance(value, float):
|
|
|
|
|
|
return "float"
|
|
|
|
|
|
elif isinstance(value, (list, dict)):
|
|
|
|
|
|
return "json"
|
|
|
|
|
|
else:
|
|
|
|
|
|
return "str"
|
|
|
|
|
|
|
|
|
|
|
|
def get_field_decimals(self, field_name):
|
|
|
|
|
|
"""获取字段的小数位数"""
|
|
|
|
|
|
return self.rules.get("field_decimals", {}).get(field_name, 6) # 默认6位
|
|
|
|
|
|
|
|
|
|
|
|
def get_validation(self, field_name):
|
|
|
|
|
|
"""获取配置项的校验规则"""
|
|
|
|
|
|
return self.rules.get("validations", {}).get(field_name, {})
|
|
|
|
|
|
|
|
|
|
|
|
def is_hidden(self, field_name):
|
|
|
|
|
|
"""检查配置项是否被标记为隐藏"""
|
|
|
|
|
|
return field_name in self.rules.get("hidden", [])
|
|
|
|
|
|
|
|
|
|
|
|
def parse_config_file(self):
|
|
|
|
|
|
"""解析配置文件,精确匹配每个配置项的位置和注释"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(self.config_file_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
content = f.read()
|
|
|
|
|
|
lines = content.split('\n')
|
|
|
|
|
|
|
|
|
|
|
|
tree = ast.parse(content)
|
|
|
|
|
|
config_data = {}
|
|
|
|
|
|
all_config_fields = {}
|
|
|
|
|
|
|
|
|
|
|
|
# 第一遍:识别所有配置项及其位置
|
|
|
|
|
|
for node in ast.walk(tree):
|
|
|
|
|
|
if isinstance(node, ast.Assign):
|
|
|
|
|
|
for target in node.targets:
|
|
|
|
|
|
if isinstance(target, ast.Name) and target.id.isupper():
|
|
|
|
|
|
var_name = target.id
|
|
|
|
|
|
|
|
|
|
|
|
# 获取配置项在文件中的位置
|
|
|
|
|
|
assignment_line = node.lineno - 1 # ast行号从1开始,我们使用0基索引
|
|
|
|
|
|
|
|
|
|
|
|
# 向上查找配置项上面的注释行
|
|
|
|
|
|
comment_lines = []
|
|
|
|
|
|
current_line = assignment_line - 1
|
|
|
|
|
|
while current_line >= 0:
|
|
|
|
|
|
line = lines[current_line].strip()
|
|
|
|
|
|
# 如果是注释行或空行,则包含在注释中
|
|
|
|
|
|
if line.startswith('#') or line == '':
|
|
|
|
|
|
comment_lines.insert(0, lines[current_line])
|
|
|
|
|
|
current_line -= 1
|
|
|
|
|
|
# 如果是多行注释的开始
|
|
|
|
|
|
elif line.startswith("'''") or line.startswith('"""'):
|
|
|
|
|
|
# 找到多行注释的起始位置
|
|
|
|
|
|
comment_start = current_line
|
|
|
|
|
|
while comment_start >= 0 and not (lines[comment_start].strip().endswith("'''") or lines[comment_start].strip().endswith('"""')):
|
|
|
|
|
|
comment_start -= 1
|
|
|
|
|
|
if comment_start >= 0:
|
|
|
|
|
|
for i in range(comment_start, current_line + 1):
|
|
|
|
|
|
comment_lines.insert(0, lines[i])
|
|
|
|
|
|
current_line = comment_start - 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
break
|
|
|
|
|
|
else:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# 完整的配置项块(包括注释和赋值行)
|
|
|
|
|
|
full_block = comment_lines + [lines[assignment_line]]
|
|
|
|
|
|
|
|
|
|
|
|
# 如果赋值行有后续行(如多行字符串),添加它们
|
|
|
|
|
|
end_line = getattr(node, 'end_lineno', node.lineno) - 1
|
|
|
|
|
|
if end_line > assignment_line:
|
|
|
|
|
|
for i in range(assignment_line + 1, end_line + 1):
|
|
|
|
|
|
full_block.append(lines[i])
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取配置项的值
|
|
|
|
|
|
var_value = eval(compile(ast.Expression(node.value), '<string>', 'eval'))
|
|
|
|
|
|
config_data[var_name] = var_value
|
|
|
|
|
|
|
|
|
|
|
|
# 获取字段类型和小数位数
|
|
|
|
|
|
field_type = self.get_field_type(var_name, var_value)
|
|
|
|
|
|
decimals = self.get_field_decimals(var_name) if field_type == "float" else None
|
|
|
|
|
|
|
|
|
|
|
|
all_config_fields[var_name] = ConfigField(
|
|
|
|
|
|
name=var_name,
|
|
|
|
|
|
value=var_value,
|
2025-12-12 09:49:12 +00:00
|
|
|
|
category=self.categorize_field(var_name), # 使用分类方法
|
2025-12-03 07:42:43 +00:00
|
|
|
|
display_name=self.get_display_name(var_name),
|
|
|
|
|
|
field_type=field_type,
|
|
|
|
|
|
decimals=decimals,
|
|
|
|
|
|
tooltip=self.get_tooltip(var_name),
|
|
|
|
|
|
hidden=self.is_hidden(var_name),
|
|
|
|
|
|
line_number=assignment_line - len(comment_lines),
|
|
|
|
|
|
original_lines=full_block,
|
|
|
|
|
|
validation=self.get_validation(var_name)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
# 如果无法解析值,使用字符串表示
|
|
|
|
|
|
try:
|
|
|
|
|
|
value_str = ast.get_source_segment(content, node.value)
|
|
|
|
|
|
config_data[var_name] = value_str
|
|
|
|
|
|
|
|
|
|
|
|
all_config_fields[var_name] = ConfigField(
|
|
|
|
|
|
name=var_name,
|
|
|
|
|
|
value=value_str,
|
2025-12-12 09:49:12 +00:00
|
|
|
|
category=self.categorize_field(var_name), # 使用分类方法
|
2025-12-03 07:42:43 +00:00
|
|
|
|
display_name=self.get_display_name(var_name),
|
|
|
|
|
|
field_type="str",
|
|
|
|
|
|
decimals=None,
|
|
|
|
|
|
tooltip=self.get_tooltip(var_name),
|
|
|
|
|
|
hidden=self.is_hidden(var_name),
|
|
|
|
|
|
line_number=assignment_line - len(comment_lines),
|
|
|
|
|
|
original_lines=full_block,
|
|
|
|
|
|
validation=self.get_validation(var_name)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
config_data[var_name] = "无法解析的值"
|
|
|
|
|
|
all_config_fields[var_name] = ConfigField(
|
|
|
|
|
|
name=var_name,
|
|
|
|
|
|
value="无法解析的值",
|
2025-12-12 09:49:12 +00:00
|
|
|
|
category=self.categorize_field(var_name), # 使用分类方法
|
2025-12-03 07:42:43 +00:00
|
|
|
|
display_name=self.get_display_name(var_name),
|
|
|
|
|
|
field_type="str",
|
|
|
|
|
|
decimals=None,
|
|
|
|
|
|
tooltip=self.get_tooltip(var_name),
|
|
|
|
|
|
hidden=self.is_hidden(var_name),
|
|
|
|
|
|
line_number=assignment_line - len(comment_lines),
|
|
|
|
|
|
original_lines=full_block,
|
|
|
|
|
|
validation=self.get_validation(var_name)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return config_data, all_config_fields, content
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
QMessageBox.critical(self, "错误", f"解析配置文件失败:{str(e)}")
|
|
|
|
|
|
return {}, {}, ""
|
|
|
|
|
|
|
|
|
|
|
|
def create_field_widget(self, config_field):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""创建字段编辑控件"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
field_type = config_field.get_actual_field_type()
|
|
|
|
|
|
|
|
|
|
|
|
if field_type == "bool":
|
|
|
|
|
|
widget = QCheckBox()
|
|
|
|
|
|
widget.setChecked(bool(config_field.value))
|
|
|
|
|
|
# 连接状态改变信号
|
|
|
|
|
|
widget.stateChanged.connect(lambda state, fn=config_field.name: self.on_field_modified(fn))
|
|
|
|
|
|
elif field_type == "int":
|
|
|
|
|
|
widget = NoWheelSpinBox() # 使用禁用了滚轮的SpinBox
|
|
|
|
|
|
widget.setRange(-1000000, 1000000)
|
|
|
|
|
|
if config_field.value is not None:
|
|
|
|
|
|
widget.setValue(int(config_field.value))
|
|
|
|
|
|
# 连接值改变信号
|
|
|
|
|
|
widget.valueChanged.connect(lambda value, fn=config_field.name: self.on_field_modified(fn))
|
|
|
|
|
|
elif field_type == "float":
|
|
|
|
|
|
widget = NoWheelDoubleSpinBox() # 使用禁用了滚轮的DoubleSpinBox
|
|
|
|
|
|
widget.setRange(-1000000.0, 1000000.0)
|
|
|
|
|
|
|
|
|
|
|
|
# 根据规则设置小数位数
|
|
|
|
|
|
decimals = config_field.decimals or 6 # 默认6位
|
|
|
|
|
|
widget.setDecimals(decimals)
|
|
|
|
|
|
|
|
|
|
|
|
if config_field.value is not None:
|
|
|
|
|
|
widget.setValue(float(config_field.value))
|
|
|
|
|
|
# 连接值改变信号
|
|
|
|
|
|
widget.valueChanged.connect(lambda value, fn=config_field.name: self.on_field_modified(fn))
|
|
|
|
|
|
elif field_type == "json":
|
|
|
|
|
|
widget = QTextEdit()
|
|
|
|
|
|
widget.setMaximumHeight(120)
|
|
|
|
|
|
if config_field.value is not None:
|
|
|
|
|
|
# 对于字典,使用漂亮的格式化显示
|
|
|
|
|
|
formatted_json = json.dumps(config_field.value, indent=4, ensure_ascii=False)
|
|
|
|
|
|
widget.setPlainText(formatted_json)
|
|
|
|
|
|
# 连接文本改变信号
|
|
|
|
|
|
widget.textChanged.connect(lambda fn=config_field.name: self.on_field_modified(fn))
|
|
|
|
|
|
else:
|
|
|
|
|
|
widget = QLineEdit()
|
|
|
|
|
|
if config_field.value is not None:
|
|
|
|
|
|
widget.setText(str(config_field.value))
|
|
|
|
|
|
# 连接文本改变信号
|
|
|
|
|
|
widget.textChanged.connect(lambda text, fn=config_field.name: self.on_field_modified(fn))
|
|
|
|
|
|
|
|
|
|
|
|
# 添加校验提示到tooltip
|
|
|
|
|
|
validation_tooltip = self.get_validation_tooltip(config_field)
|
|
|
|
|
|
full_tooltip = config_field.tooltip
|
|
|
|
|
|
if validation_tooltip:
|
|
|
|
|
|
full_tooltip += f"\n\n校验规则:\n{validation_tooltip}"
|
|
|
|
|
|
|
|
|
|
|
|
widget.setToolTip(full_tooltip)
|
|
|
|
|
|
widget.setMinimumWidth(300)
|
|
|
|
|
|
return widget
|
|
|
|
|
|
|
|
|
|
|
|
def get_validation_tooltip(self, config_field):
|
|
|
|
|
|
"""生成校验规则的提示信息"""
|
|
|
|
|
|
validation = config_field.validation
|
|
|
|
|
|
if not validation:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
rules = []
|
|
|
|
|
|
if validation.get("required"):
|
|
|
|
|
|
rules.append("• 必填项")
|
|
|
|
|
|
if "min" in validation:
|
|
|
|
|
|
rules.append(f"• 最小值: {validation['min']}")
|
|
|
|
|
|
if "max" in validation:
|
|
|
|
|
|
rules.append(f"• 最大值: {validation['max']}")
|
|
|
|
|
|
if "regex" in validation:
|
|
|
|
|
|
rules.append(f"• 正则表达式: {validation['regex']}")
|
|
|
|
|
|
|
|
|
|
|
|
return "\n".join(rules)
|
|
|
|
|
|
|
|
|
|
|
|
def on_field_modified(self, field_name):
|
|
|
|
|
|
"""当字段值被修改时的处理"""
|
|
|
|
|
|
if field_name not in self.modified_fields:
|
|
|
|
|
|
self.modified_fields.add(field_name)
|
|
|
|
|
|
# 高亮显示修改的字段
|
|
|
|
|
|
self.highlight_modified_field(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
# 更新状态栏显示修改数量
|
|
|
|
|
|
self.update_status_bar()
|
|
|
|
|
|
|
|
|
|
|
|
def highlight_modified_field(self, field_name):
|
|
|
|
|
|
"""高亮显示被修改的字段,但不覆盖子控件样式"""
|
|
|
|
|
|
if field_name in self.field_containers:
|
|
|
|
|
|
container = self.field_containers[field_name]
|
|
|
|
|
|
# 只修改背景色,使用更精确的选择器确保不影响子控件
|
|
|
|
|
|
container.setStyleSheet("""
|
|
|
|
|
|
QFrame#field_container {
|
|
|
|
|
|
background-color: #fff9c4;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
def clear_field_highlight(self, field_name):
|
|
|
|
|
|
"""清除字段的高亮显示,恢复原有样式"""
|
|
|
|
|
|
if field_name in self.field_containers:
|
|
|
|
|
|
container = self.field_containers[field_name]
|
|
|
|
|
|
# 恢复原始样式,不影响子控件
|
|
|
|
|
|
container.setStyleSheet("""
|
|
|
|
|
|
QFrame#field_container {
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
def update_status_bar(self):
|
|
|
|
|
|
"""更新状态栏显示修改数量"""
|
|
|
|
|
|
if self.modified_fields:
|
|
|
|
|
|
self.statusBar().showMessage(f"已修改 {len(self.modified_fields)} 个配置项")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.statusBar().showMessage("就绪")
|
|
|
|
|
|
|
|
|
|
|
|
def create_field_editor(self, config_field):
|
|
|
|
|
|
"""创建字段编辑器,返回容器和控件"""
|
|
|
|
|
|
# 创建容器框架
|
|
|
|
|
|
container = QFrame()
|
|
|
|
|
|
container.setObjectName("field_container") # 设置对象名用于样式选择
|
|
|
|
|
|
container.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
|
|
|
# 设置基础样式,使用对象名选择器确保只影响容器本身
|
|
|
|
|
|
container.setStyleSheet("""
|
|
|
|
|
|
QFrame#field_container {
|
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
layout = QHBoxLayout(container)
|
|
|
|
|
|
layout.setContentsMargins(8, 4, 8, 4)
|
|
|
|
|
|
layout.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
# 标签
|
|
|
|
|
|
label = QLabel(config_field.display_name)
|
|
|
|
|
|
label.setMinimumWidth(180)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加校验指示器(如果配置项有校验规则)
|
|
|
|
|
|
validation_indicator = ""
|
|
|
|
|
|
if config_field.validation:
|
|
|
|
|
|
if config_field.validation.get("required"):
|
|
|
|
|
|
validation_indicator = " *"
|
|
|
|
|
|
label.setStyleSheet("color: #d32f2f; font-weight: bold;")
|
|
|
|
|
|
else:
|
|
|
|
|
|
label.setStyleSheet("color: #1976d2;")
|
|
|
|
|
|
|
|
|
|
|
|
label.setText(f"{config_field.display_name}{validation_indicator}")
|
|
|
|
|
|
label.setToolTip(self.get_validation_tooltip(config_field))
|
|
|
|
|
|
|
|
|
|
|
|
# 如果配置项是隐藏的,添加视觉提示
|
|
|
|
|
|
if config_field.hidden:
|
|
|
|
|
|
label.setStyleSheet("color: #999999; font-style: italic;")
|
|
|
|
|
|
label.setText(f"{config_field.display_name} [隐藏]")
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(label)
|
|
|
|
|
|
|
|
|
|
|
|
# 控件
|
|
|
|
|
|
widget = self.create_field_widget(config_field)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果配置项是隐藏的,禁用控件
|
|
|
|
|
|
if config_field.hidden:
|
|
|
|
|
|
widget.setEnabled(False)
|
|
|
|
|
|
widget.setStyleSheet("background-color: #f0f0f0; color: #999999;")
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(widget)
|
|
|
|
|
|
|
|
|
|
|
|
# 变量名显示
|
|
|
|
|
|
name_label = QLabel(f"({config_field.name})")
|
|
|
|
|
|
name_label.setStyleSheet("color: #666666; font-size: 10px; font-style: italic;")
|
|
|
|
|
|
name_label.setMinimumWidth(120)
|
|
|
|
|
|
name_label.setToolTip(f"配置变量名: {config_field.name}")
|
|
|
|
|
|
layout.addWidget(name_label)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
# 存储容器引用
|
|
|
|
|
|
self.field_containers[config_field.name] = container
|
|
|
|
|
|
|
|
|
|
|
|
return container, widget
|
|
|
|
|
|
|
|
|
|
|
|
def generate_dynamic_ui(self, show_hidden=False):
|
|
|
|
|
|
"""生成动态UI,可选择是否显示隐藏的配置项"""
|
|
|
|
|
|
self.dynamic_widgets.clear()
|
|
|
|
|
|
self.modified_fields.clear() # 重置修改记录
|
|
|
|
|
|
self.field_containers.clear() # 清空容器引用
|
|
|
|
|
|
|
|
|
|
|
|
for category, layout in self.category_widgets.items():
|
|
|
|
|
|
while layout.count():
|
|
|
|
|
|
child = layout.takeAt(0)
|
|
|
|
|
|
if child.widget():
|
|
|
|
|
|
child.widget().deleteLater()
|
|
|
|
|
|
|
|
|
|
|
|
# 根据是否显示隐藏项,选择使用哪个配置项集合
|
|
|
|
|
|
config_fields_to_use = self.all_config_fields if show_hidden else self.config_fields
|
|
|
|
|
|
|
|
|
|
|
|
categorized_fields = {}
|
|
|
|
|
|
for field_name, config_field in config_fields_to_use.items():
|
|
|
|
|
|
category = config_field.category
|
|
|
|
|
|
if category not in categorized_fields:
|
|
|
|
|
|
categorized_fields[category] = []
|
|
|
|
|
|
categorized_fields[category].append(config_field)
|
|
|
|
|
|
|
|
|
|
|
|
for category, fields in categorized_fields.items():
|
|
|
|
|
|
if category not in self.category_widgets:
|
|
|
|
|
|
self.create_category_tab(category)
|
|
|
|
|
|
|
|
|
|
|
|
layout = self.category_widgets[category]
|
|
|
|
|
|
fields.sort(key=lambda x: x.name)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用GroupBox,更清晰的分组
|
2025-12-12 09:49:12 +00:00
|
|
|
|
group_box = QGroupBox(f"{category}")
|
2025-12-03 07:42:43 +00:00
|
|
|
|
group_layout = QVBoxLayout(group_box)
|
|
|
|
|
|
group_layout.setSpacing(6)
|
|
|
|
|
|
|
|
|
|
|
|
for config_field in fields:
|
|
|
|
|
|
container, widget = self.create_field_editor(config_field)
|
|
|
|
|
|
group_layout.addWidget(container)
|
|
|
|
|
|
self.dynamic_widgets[config_field.name] = (widget, config_field.get_actual_field_type())
|
|
|
|
|
|
|
|
|
|
|
|
group_layout.addStretch()
|
|
|
|
|
|
layout.addWidget(group_box)
|
|
|
|
|
|
|
|
|
|
|
|
def load_config(self):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""加载配置文件"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
try:
|
|
|
|
|
|
# 检查配置文件是否存在
|
|
|
|
|
|
if not self.check_config_file():
|
|
|
|
|
|
QMessageBox.warning(self, "警告", f"配置文件不存在: {self.config_file_path}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.config_data, self.all_config_fields, self.original_content = self.parse_config_file()
|
|
|
|
|
|
|
|
|
|
|
|
if not self.config_data:
|
|
|
|
|
|
QMessageBox.warning(self, "警告", "未找到任何配置项")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 保存原始配置数据用于比较
|
|
|
|
|
|
self.original_config_data = self.config_data.copy()
|
|
|
|
|
|
|
|
|
|
|
|
# 过滤掉隐藏的配置项
|
|
|
|
|
|
self.config_fields = {k: v for k, v in self.all_config_fields.items() if not v.hidden}
|
|
|
|
|
|
|
|
|
|
|
|
# 根据当前显示状态生成UI
|
|
|
|
|
|
show_hidden = getattr(self, 'show_hidden', False)
|
|
|
|
|
|
self.generate_dynamic_ui(show_hidden)
|
|
|
|
|
|
|
|
|
|
|
|
hidden_count = len(self.all_config_fields) - len(self.config_fields)
|
2025-12-12 09:49:12 +00:00
|
|
|
|
self.statusBar().showMessage(f"成功加载 {len(self.config_data)} 个配置项 ({len(self.config_fields)} 个显示, {hidden_count} 个隐藏)")
|
2025-12-03 07:42:43 +00:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
QMessageBox.critical(self, "错误", f"加载配置文件失败:{str(e)}")
|
|
|
|
|
|
self.statusBar().showMessage("加载配置文件失败")
|
|
|
|
|
|
|
|
|
|
|
|
def toggle_hidden_fields(self, checked):
|
|
|
|
|
|
"""切换显示/隐藏配置项"""
|
|
|
|
|
|
self.show_hidden = checked
|
|
|
|
|
|
self.generate_dynamic_ui(checked)
|
|
|
|
|
|
|
|
|
|
|
|
# 更新按钮文本
|
|
|
|
|
|
if checked:
|
|
|
|
|
|
self.toggle_hidden_btn.setText("隐藏隐藏项")
|
|
|
|
|
|
self.statusBar().showMessage("已显示所有配置项(包括隐藏的)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.toggle_hidden_btn.setText("显示隐藏项")
|
|
|
|
|
|
self.statusBar().showMessage("已隐藏标记为隐藏的配置项")
|
|
|
|
|
|
|
2025-12-12 09:49:12 +00:00
|
|
|
|
def hide_all_fields(self):
|
|
|
|
|
|
"""一键隐藏所有配置项"""
|
|
|
|
|
|
reply = QMessageBox.question(
|
|
|
|
|
|
self, "确认一键隐藏",
|
|
|
|
|
|
"确定要隐藏所有配置项吗?\n隐藏后,页面上将不显示任何配置项。\n如需显示,需要在规则管理中取消隐藏。",
|
|
|
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
# 获取所有配置项的名称
|
|
|
|
|
|
all_field_names = list(self.all_config_fields.keys())
|
|
|
|
|
|
|
|
|
|
|
|
# 将所有配置项添加到隐藏列表
|
|
|
|
|
|
self.rules["hidden"] = all_field_names
|
|
|
|
|
|
|
|
|
|
|
|
# 保存规则
|
|
|
|
|
|
self.save_rules()
|
|
|
|
|
|
|
|
|
|
|
|
# 重新加载配置
|
|
|
|
|
|
self.load_config()
|
|
|
|
|
|
|
|
|
|
|
|
# 更新状态栏
|
|
|
|
|
|
self.statusBar().showMessage("已隐藏所有配置项,页面已清空")
|
|
|
|
|
|
|
|
|
|
|
|
QMessageBox.information(self, "完成", "已成功隐藏所有配置项。\n如需显示配置项,请在规则管理中取消隐藏。")
|
|
|
|
|
|
|
2025-12-03 07:42:43 +00:00
|
|
|
|
def manage_rules(self):
|
2025-12-12 09:49:12 +00:00
|
|
|
|
"""管理规则"""
|
2025-12-03 07:42:43 +00:00
|
|
|
|
dialog = ConfigManagementWindow(self.rules, self.all_config_fields, self)
|
|
|
|
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
|
|
|
|
updated_rules = dialog.get_updated_rules()
|
|
|
|
|
|
self.rules.update(updated_rules)
|
|
|
|
|
|
self.save_rules()
|
|
|
|
|
|
self.load_config()
|
|
|
|
|
|
QMessageBox.information(self, "成功", "规则已更新并应用!")
|
|
|
|
|
|
|
|
|
|
|
|
def collect_ui_data(self):
|
|
|
|
|
|
"""从UI收集数据"""
|
|
|
|
|
|
ui_data = {}
|
|
|
|
|
|
for field_name, (widget, field_type) in self.dynamic_widgets.items():
|
|
|
|
|
|
# 跳过隐藏且禁用的配置项
|
|
|
|
|
|
if not widget.isEnabled():
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if field_type == "bool":
|
|
|
|
|
|
ui_data[field_name] = widget.isChecked()
|
|
|
|
|
|
elif field_type == "int":
|
|
|
|
|
|
ui_data[field_name] = widget.value()
|
|
|
|
|
|
elif field_type == "float":
|
|
|
|
|
|
ui_data[field_name] = widget.value()
|
|
|
|
|
|
elif field_type == "json":
|
|
|
|
|
|
try:
|
|
|
|
|
|
text = widget.toPlainText().strip()
|
|
|
|
|
|
if text:
|
|
|
|
|
|
ui_data[field_name] = json.loads(text)
|
|
|
|
|
|
else:
|
|
|
|
|
|
ui_data[field_name] = {}
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
QMessageBox.warning(self, "JSON格式错误",
|
|
|
|
|
|
f"配置项 {field_name} 的JSON格式不正确")
|
|
|
|
|
|
return None
|
|
|
|
|
|
else:
|
|
|
|
|
|
text = widget.text().strip()
|
|
|
|
|
|
ui_data[field_name] = text
|
|
|
|
|
|
|
|
|
|
|
|
return ui_data
|
|
|
|
|
|
|
|
|
|
|
|
def validate_config_data(self, config_data):
|
|
|
|
|
|
"""校验配置数据"""
|
|
|
|
|
|
errors = []
|
|
|
|
|
|
|
|
|
|
|
|
for field_name, value in config_data.items():
|
|
|
|
|
|
config_field = self.all_config_fields.get(field_name)
|
|
|
|
|
|
if not config_field:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
validation = config_field.validation
|
|
|
|
|
|
if not validation:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 必填校验
|
|
|
|
|
|
if validation.get("required") and (value is None or value == ""):
|
|
|
|
|
|
errors.append(f"配置项 '{config_field.display_name}' 是必填项")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 数字范围校验
|
|
|
|
|
|
if isinstance(value, (int, float)):
|
|
|
|
|
|
if "min" in validation:
|
|
|
|
|
|
try:
|
|
|
|
|
|
min_val = float(validation["min"])
|
|
|
|
|
|
if value < min_val:
|
|
|
|
|
|
errors.append(f"配置项 '{config_field.display_name}' 的值不能小于 {min_val}")
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if "max" in validation:
|
|
|
|
|
|
try:
|
|
|
|
|
|
max_val = float(validation["max"])
|
|
|
|
|
|
if value > max_val:
|
|
|
|
|
|
errors.append(f"配置项 '{config_field.display_name}' 的值不能大于 {max_val}")
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 字符串正则校验
|
|
|
|
|
|
elif isinstance(value, str) and "regex" in validation:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not re.match(validation["regex"], value):
|
|
|
|
|
|
errors.append(f"配置项 '{config_field.display_name}' 的值不符合格式要求")
|
|
|
|
|
|
except re.error:
|
|
|
|
|
|
errors.append(f"配置项 '{config_field.display_name}' 的正则表达式格式错误")
|
|
|
|
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
|
|
|
|
|
|
|
def get_changes(self, new_data):
|
|
|
|
|
|
"""获取配置变更列表"""
|
|
|
|
|
|
changes = {}
|
|
|
|
|
|
for field_name, new_value in new_data.items():
|
|
|
|
|
|
old_value = self.original_config_data.get(field_name)
|
|
|
|
|
|
if old_value != new_value:
|
|
|
|
|
|
changes[field_name] = (old_value, new_value)
|
|
|
|
|
|
return changes
|
|
|
|
|
|
|
|
|
|
|
|
def format_dict_value(self, value, original_lines=None):
|
|
|
|
|
|
"""专门格式化字典值,保持多行格式"""
|
|
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果有原始行信息,尝试保持原始格式
|
|
|
|
|
|
if original_lines and len(original_lines) > 1:
|
|
|
|
|
|
# 检查原始格式是否是漂亮的多行格式
|
|
|
|
|
|
first_line = original_lines[0].strip()
|
|
|
|
|
|
if first_line.endswith('{') and len(original_lines) > 2:
|
|
|
|
|
|
# 原始是多行格式,我们也使用多行格式
|
|
|
|
|
|
result = "{\n"
|
|
|
|
|
|
for i, (key, val) in enumerate(value.items()):
|
|
|
|
|
|
# 使用4个空格缩进
|
|
|
|
|
|
result += f' "{key}": {json.dumps(val, ensure_ascii=False)}'
|
|
|
|
|
|
if i < len(value) - 1:
|
|
|
|
|
|
result += ",\n"
|
|
|
|
|
|
else:
|
|
|
|
|
|
result += "\n"
|
|
|
|
|
|
result += "}"
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
# 如果没有原始格式信息或不是多行格式,使用漂亮的JSON格式
|
|
|
|
|
|
return json.dumps(value, indent=4, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
def format_value(self, value, config_field=None):
|
|
|
|
|
|
"""格式化值,保持与Python兼容的格式"""
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
return "None"
|
|
|
|
|
|
elif isinstance(value, bool):
|
|
|
|
|
|
return "True" if value else "False"
|
|
|
|
|
|
elif isinstance(value, (int, float)):
|
|
|
|
|
|
# 根据小数位数格式化浮点数
|
|
|
|
|
|
if isinstance(value, float) and config_field and config_field.decimals is not None:
|
|
|
|
|
|
# 格式化浮点数,保留指定小数位数
|
|
|
|
|
|
format_str = f"{{:.{config_field.decimals}f}}"
|
|
|
|
|
|
return format_str.format(value)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
elif isinstance(value, str):
|
|
|
|
|
|
# 检查字符串是否已经用引号包围
|
|
|
|
|
|
if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
|
|
|
|
|
|
return value
|
|
|
|
|
|
# 检查字符串是否包含特殊字符
|
|
|
|
|
|
elif any(char in value for char in [' ', '\t', '\n', '#', '=']):
|
|
|
|
|
|
return f'"{value}"'
|
|
|
|
|
|
else:
|
|
|
|
|
|
return f'"{value}"'
|
|
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
|
|
# 对于字典,使用专门的格式化函数
|
|
|
|
|
|
return self.format_dict_value(value, config_field.original_lines if config_field else None)
|
|
|
|
|
|
elif isinstance(value, list):
|
|
|
|
|
|
return json.dumps(value, ensure_ascii=False, indent=4)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return repr(value)
|
|
|
|
|
|
|
|
|
|
|
|
def save_config(self):
|
|
|
|
|
|
"""保存配置,保留所有注释、空格和格式"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 检查配置文件是否存在
|
|
|
|
|
|
if not self.check_config_file():
|
|
|
|
|
|
QMessageBox.warning(self, "警告", f"配置文件不存在: {self.config_file_path}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 收集UI数据
|
|
|
|
|
|
new_data = self.collect_ui_data()
|
|
|
|
|
|
if new_data is None: # JSON解析错误
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 校验配置数据
|
|
|
|
|
|
validation_errors = self.validate_config_data(new_data)
|
|
|
|
|
|
if validation_errors:
|
|
|
|
|
|
error_msg = "配置校验失败:\n\n" + "\n".join(validation_errors)
|
|
|
|
|
|
QMessageBox.warning(self, "配置校验失败", error_msg)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 获取变更列表
|
|
|
|
|
|
changes = self.get_changes(new_data)
|
|
|
|
|
|
|
|
|
|
|
|
if not changes:
|
|
|
|
|
|
QMessageBox.information(self, "提示", "没有检测到任何配置变更")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 显示变更确认对话框
|
|
|
|
|
|
dialog = ChangeConfirmDialog(changes, self)
|
|
|
|
|
|
result = dialog.exec()
|
|
|
|
|
|
|
|
|
|
|
|
if result != QDialog.DialogCode.Accepted:
|
|
|
|
|
|
self.statusBar().showMessage("用户取消了保存操作")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 读取原始文件内容
|
|
|
|
|
|
with open(self.config_file_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
original_content = f.read()
|
|
|
|
|
|
original_lines = original_content.split('\n')
|
|
|
|
|
|
|
|
|
|
|
|
# 构建新文件内容 - 只修改值部分,保留所有其他内容
|
|
|
|
|
|
new_lines = original_lines.copy()
|
|
|
|
|
|
|
|
|
|
|
|
# 对每个变更的配置项,找到其位置并修改值,保留空格和格式
|
|
|
|
|
|
for field_name, (old_value, new_value) in changes.items():
|
|
|
|
|
|
config_field = self.all_config_fields.get(field_name)
|
|
|
|
|
|
if not config_field or config_field.line_number is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 获取配置项的原始块
|
|
|
|
|
|
original_block = config_field.original_lines
|
|
|
|
|
|
|
|
|
|
|
|
# 查找赋值行在原始块中的位置
|
|
|
|
|
|
assignment_line_index = -1
|
|
|
|
|
|
for i, line in enumerate(original_block):
|
|
|
|
|
|
stripped = line.strip()
|
|
|
|
|
|
if stripped.startswith(field_name) and '=' in stripped:
|
|
|
|
|
|
assignment_line_index = i
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if assignment_line_index >= 0:
|
|
|
|
|
|
# 获取原始赋值行的完整内容(包括注释等)
|
|
|
|
|
|
original_assignment_line = original_block[assignment_line_index]
|
|
|
|
|
|
|
|
|
|
|
|
# 使用改进的正则表达式匹配赋值语句
|
|
|
|
|
|
# 这个正则表达式会捕获:
|
|
|
|
|
|
# 1. 变量名前的所有空格
|
|
|
|
|
|
# 2. 变量名
|
|
|
|
|
|
# 3. 等号前的空格
|
|
|
|
|
|
# 4. 等号
|
|
|
|
|
|
# 5. 等号后的空格
|
|
|
|
|
|
# 6. 原始值(直到注释或行尾)
|
|
|
|
|
|
# 7. 行尾注释(如果有)
|
|
|
|
|
|
pattern = rf'^(\s*)({re.escape(field_name)})(\s*)(=)(\s*)(.*?)(\s*(#.*)?)$'
|
|
|
|
|
|
match = re.match(pattern, original_assignment_line)
|
|
|
|
|
|
|
|
|
|
|
|
if match:
|
|
|
|
|
|
# 获取匹配的各个部分
|
|
|
|
|
|
leading_spaces = match.group(1) # 变量名前的空格
|
|
|
|
|
|
var_name_part = match.group(2) # 变量名
|
|
|
|
|
|
spaces_before_eq = match.group(3) # 等号前的空格
|
|
|
|
|
|
eq_sign = match.group(4) # 等号
|
|
|
|
|
|
spaces_after_eq = match.group(5) # 等号后的空格
|
|
|
|
|
|
old_value_part = match.group(6) # 原始值
|
|
|
|
|
|
trailing_comment = match.group(7) if match.group(7) else '' # 行尾注释
|
|
|
|
|
|
|
|
|
|
|
|
# 格式化新值
|
|
|
|
|
|
formatted_value = self.format_value(new_value, config_field)
|
|
|
|
|
|
|
|
|
|
|
|
# 构建新行,保持原始的空格格式
|
|
|
|
|
|
new_line = (f"{leading_spaces}{var_name_part}{spaces_before_eq}"
|
|
|
|
|
|
f"{eq_sign}{spaces_after_eq}{formatted_value}"
|
|
|
|
|
|
f"{trailing_comment}")
|
|
|
|
|
|
|
|
|
|
|
|
# 找到赋值行在原始文件中的实际位置
|
|
|
|
|
|
# assignment_line_in_file 是赋值行在原始文件中的行号(从0开始)
|
|
|
|
|
|
assignment_line_in_file = config_field.line_number + assignment_line_index
|
|
|
|
|
|
|
|
|
|
|
|
if assignment_line_in_file < len(new_lines):
|
|
|
|
|
|
new_lines[assignment_line_in_file] = new_line
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果正则匹配失败,使用简单的替换方法
|
|
|
|
|
|
# 查找赋值行在原始文件中的位置
|
|
|
|
|
|
for i in range(len(new_lines)):
|
|
|
|
|
|
line = new_lines[i]
|
|
|
|
|
|
stripped = line.strip()
|
|
|
|
|
|
if stripped.startswith(field_name) and '=' in stripped:
|
|
|
|
|
|
# 使用更简单的正则表达式
|
|
|
|
|
|
pattern = rf'^(\s*{re.escape(field_name)}\s*=\s*).*$'
|
|
|
|
|
|
match = re.match(pattern, line.rstrip())
|
|
|
|
|
|
if match:
|
|
|
|
|
|
# 格式化新值
|
|
|
|
|
|
formatted_value = self.format_value(new_value, config_field)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否有行尾注释
|
|
|
|
|
|
comment_pos = line.find('#')
|
|
|
|
|
|
if comment_pos > 0:
|
|
|
|
|
|
# 有行尾注释,保留注释
|
|
|
|
|
|
line_comment = line[comment_pos:]
|
|
|
|
|
|
prefix = match.group(1)
|
|
|
|
|
|
new_line = prefix + formatted_value + line_comment
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 没有行尾注释
|
|
|
|
|
|
prefix = match.group(1)
|
|
|
|
|
|
new_line = prefix + formatted_value
|
|
|
|
|
|
|
|
|
|
|
|
new_lines[i] = new_line
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# 写入文件,保持原有的换行符格式
|
|
|
|
|
|
with open(self.config_file_path, 'w', encoding='utf-8', newline='') as f:
|
|
|
|
|
|
f.write('\n'.join(new_lines))
|
|
|
|
|
|
|
|
|
|
|
|
# 清除所有高亮显示
|
|
|
|
|
|
for field_name in self.modified_fields.copy():
|
|
|
|
|
|
self.clear_field_highlight(field_name)
|
|
|
|
|
|
self.modified_fields.remove(field_name)
|
|
|
|
|
|
|
|
|
|
|
|
# 重新加载配置
|
|
|
|
|
|
self.load_config()
|
|
|
|
|
|
|
|
|
|
|
|
self.statusBar().showMessage(f"配置文件保存成功,修改了 {len(changes)} 个配置项")
|
|
|
|
|
|
|
|
|
|
|
|
# 使用定时器延迟显示成功消息
|
|
|
|
|
|
QTimer.singleShot(100, lambda: QMessageBox.information(
|
|
|
|
|
|
self, "成功", f"配置文件保存成功!\n修改了 {len(changes)} 个配置项"
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.statusBar().showMessage("保存配置文件失败")
|
|
|
|
|
|
QMessageBox.critical(self, "错误", f"保存配置文件失败:{str(e)}\n\n错误详情:{traceback.format_exc()}")
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
|
|
app.setStyle('Fusion')
|
|
|
|
|
|
|
|
|
|
|
|
# 创建编辑器
|
|
|
|
|
|
editor = ConfigEditor()
|
|
|
|
|
|
editor.show()
|
|
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
main()
|