From 95b27a2d60a419bef82a3be4414fdae2b6a65366 Mon Sep 17 00:00:00 2001 From: hujinyang Date: Wed, 31 Dec 2025 09:23:12 +0000 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20config=5Feditor.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config_editor.py | 1243 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 1137 insertions(+), 106 deletions(-) diff --git a/config_editor.py b/config_editor.py index 2673184..2576834 100644 --- a/config_editor.py +++ b/config_editor.py @@ -4,16 +4,18 @@ import ast import json import re import traceback +import datetime +import shutil 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 + QGroupBox, QSplitter, QListWidget, QListWidgetItem, QComboBox, + QInputDialog, QFileDialog, QMenuBar, QMenu) +from PyQt6.QtCore import Qt, QSize, QTimer, QEvent, QMimeData +from PyQt6.QtGui import QAction, QDrag, QColor, QBrush, QFont class ConfigSettingsDialog(QDialog): """配置文件设置对话框""" @@ -124,11 +126,18 @@ class ConfigManagementWindow(QDialog): left_layout = QVBoxLayout(left_widget) left_layout.setContentsMargins(4, 4, 4, 4) - # 搜索框 + # 搜索框 - 添加搜索类型选择 search_layout = QHBoxLayout() search_layout.addWidget(QLabel("搜索:")) + + # 搜索类型选择 + self.search_type_combo = QComboBox() + self.search_type_combo.addItems(["变量名", "显示名称"]) + self.search_type_combo.setMaximumWidth(80) + search_layout.addWidget(self.search_type_combo) + self.search_edit = QLineEdit() - self.search_edit.setPlaceholderText("输入配置项名称进行搜索...") + self.search_edit.setPlaceholderText("输入关键词进行搜索...") self.search_edit.textChanged.connect(self.filter_fields) search_layout.addWidget(self.search_edit) @@ -140,7 +149,11 @@ class ConfigManagementWindow(QDialog): left_layout.addLayout(search_layout) - left_layout.addWidget(QLabel("配置项列表:")) + # 修改:添加分组显示说明 + list_label = QLabel("配置项列表(上方为未隐藏项,下方为隐藏项):") + list_label.setStyleSheet("color: #333333; font-weight: bold;") + left_layout.addWidget(list_label) + self.fields_list = QListWidget() self.fields_list.currentItemChanged.connect(self.on_field_selected) left_layout.addWidget(self.fields_list) @@ -199,6 +212,7 @@ class ConfigManagementWindow(QDialog): # 隐藏状态 self.hidden_checkbox = QCheckBox("隐藏此配置项(不在主界面显示)") + self.hidden_checkbox.stateChanged.connect(self.on_hidden_changed) form_layout.addWidget(self.hidden_checkbox) # 提示信息 @@ -278,7 +292,24 @@ class ConfigManagementWindow(QDialog): batch_btn_layout.addStretch() batch_layout.addLayout(batch_btn_layout) - right_layout.addWidget(batch_group) + + # === 新增:导入导出功能 === + import_export_group = QGroupBox("导入导出规则文件") + import_export_layout = QVBoxLayout(import_export_group) + import_export_layout.setContentsMargins(8, 8, 8, 8) + + import_export_btn_layout = QHBoxLayout() + self.import_rules_btn = QPushButton("导入规则文件") + self.import_rules_btn.clicked.connect(self.import_rules_file) + self.export_rules_btn = QPushButton("导出规则文件") + self.export_rules_btn.clicked.connect(self.export_rules_file) + + import_export_btn_layout.addWidget(self.import_rules_btn) + import_export_btn_layout.addWidget(self.export_rules_btn) + import_export_btn_layout.addStretch() + + import_export_layout.addLayout(import_export_btn_layout) + right_layout.addWidget(import_export_group) right_layout.addStretch() splitter.addWidget(right_widget) @@ -303,10 +334,46 @@ class ConfigManagementWindow(QDialog): layout.addLayout(row_layout) def load_data(self): - """加载数据到界面""" + """加载数据到界面(按隐藏状态分组显示)""" self.fields_list.clear() + + # 获取隐藏字段列表 + hidden_fields = self.rules.get("hidden", []) + + # 分离未隐藏和已隐藏的字段 + visible_fields = [] + hidden_fields_list = [] + for field_name in sorted(self.config_fields.keys()): - self.fields_list.addItem(field_name) + if field_name in hidden_fields: + hidden_fields_list.append(field_name) + else: + visible_fields.append(field_name) + + # 添加分隔项 + if visible_fields and hidden_fields_list: + # 添加分隔符(未隐藏部分) + self.create_separator_item("未隐藏的配置项") + + # 添加未隐藏的字段(高亮显示) + for field_name in visible_fields: + self.create_field_item(field_name, is_hidden=False) + + # 添加分隔项 + if visible_fields and hidden_fields_list: + # 添加分隔符(隐藏部分) + self.create_separator_item("已隐藏的配置项") + + # 添加已隐藏的字段(灰色显示) + for field_name in hidden_fields_list: + self.create_field_item(field_name, is_hidden=True) + + # 如果没有配置项,显示提示 + if not visible_fields and not hidden_fields_list: + item = QListWidgetItem("没有配置项") + self.fields_list.addItem(item) + item.setFlags(Qt.ItemFlag.NoItemFlags) # 不可选 + item.setForeground(QBrush(QColor("#999999"))) self.update_fields_count() @@ -320,44 +387,119 @@ class ConfigManagementWindow(QDialog): for category in sorted(categories): self.category_combo.addItem(category) + def create_separator_item(self, text): + """创建分隔项""" + item = QListWidgetItem(f"--- {text} ---") + self.fields_list.addItem(item) + item.setFlags(Qt.ItemFlag.NoItemFlags) # 不可选 + item.setForeground(QBrush(QColor("#666666"))) + font = item.font() + font.setItalic(True) + font.setBold(True) + item.setFont(font) + + # 设置背景色 + item.setBackground(QBrush(QColor("#f0f0f0"))) + return item + + def create_field_item(self, field_name, is_hidden=False): + """创建字段项""" + item = QListWidgetItem(field_name) + self.fields_list.addItem(item) + + # 获取显示名称用于搜索 + display_name = self.rules.get("display_names", {}).get(field_name, field_name) + + # 设置自定义数据:变量名和显示名称 + item.setData(Qt.ItemDataRole.UserRole, field_name) # 变量名 + item.setData(Qt.ItemDataRole.UserRole + 1, display_name) # 显示名称 + + if is_hidden: + # 隐藏字段:灰色,斜体 + item.setForeground(QBrush(QColor("#999999"))) + font = item.font() + font.setItalic(True) + item.setFont(font) + item.setToolTip(f"已隐藏: {field_name} ({display_name})") + else: + # 未隐藏字段:高亮显示(浅绿色背景) + item.setBackground(QBrush(QColor("#e8f5e9"))) # 浅绿色背景 + font = item.font() + font.setBold(True) + item.setFont(font) + item.setToolTip(f"未隐藏: {field_name} ({display_name})") + + return item + def filter_fields(self, search_text): - """根据搜索文本过滤配置项列表""" + """根据搜索文本过滤配置项列表(保持分组),支持变量名和显示名称搜索""" search_text = search_text.strip().lower() + search_type = self.search_type_combo.currentText() # 获取搜索类型 if not search_text: # 如果搜索文本为空,显示所有项 for i in range(self.fields_list.count()): item = self.fields_list.item(i) - item.setHidden(False) + # 跳过分隔符项 + if not self.is_separator_item(item): + 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) + # 跳过分隔符项 + if not self.is_separator_item(item): + # 根据搜索类型获取要匹配的文本 + if search_type == "变量名": + # 搜索变量名 + field_name = item.data(Qt.ItemDataRole.UserRole) + match_text = field_name.lower() if field_name else "" + else: # 显示名称 + # 搜索显示名称 + display_name = item.data(Qt.ItemDataRole.UserRole + 1) + match_text = display_name.lower() if display_name else "" + + # 模糊匹配:检查搜索文本是否在匹配文本中 + if search_text in match_text: + item.setHidden(False) + else: + item.setHidden(True) self.update_fields_count() + def is_separator_item(self, item): + """检查是否是分隔符项""" + text = item.text() + return text.startswith("---") and text.endswith("---") + def clear_search(self): """清除搜索框内容""" self.search_edit.clear() def update_fields_count(self): """更新配置项计数显示""" - total_count = self.fields_list.count() + total_count = 0 visible_count = 0 + hidden_count = 0 - for i in range(total_count): - if not self.fields_list.item(i).isHidden(): - visible_count += 1 + for i in range(self.fields_list.count()): + item = self.fields_list.item(i) + # 跳过分隔符项 + if not self.is_separator_item(item): + total_count += 1 + if not item.isHidden(): + visible_count += 1 + + # 统计隐藏状态 + if "已隐藏" in item.toolTip(): + hidden_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} 个配置项") + visible_unhidden = visible_count - hidden_count + visible_hidden = hidden_count + + # 显示搜索类型信息 + search_type = self.search_type_combo.currentText() + self.fields_count_label.setText(f"总计: {total_count} 个配置项 (显示: {visible_unhidden} 未隐藏, {visible_hidden} 隐藏) - 搜索类型: {search_type}") def on_type_changed(self, field_type): """当类型改变时,控制小数位数设置的显示""" @@ -371,20 +513,69 @@ class ConfigManagementWindow(QDialog): 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: + def on_hidden_changed(self, state): + """当隐藏状态改变时,更新左侧列表项的外观和位置""" + current_item = self.fields_list.currentItem() + if not current_item or self.is_separator_item(current_item): return - field_name = current.text() + field_name = current_item.data(Qt.ItemDataRole.UserRole) + display_name = current_item.data(Qt.ItemDataRole.UserRole + 1) + is_hidden = (state == Qt.CheckState.Checked.value) + + # 更新工具提示 + if is_hidden: + current_item.setToolTip(f"已隐藏: {field_name} ({display_name})") + else: + current_item.setToolTip(f"未隐藏: {field_name} ({display_name})") + + # 更新外观 + if is_hidden: + # 改为隐藏样式 + current_item.setForeground(QBrush(QColor("#999999"))) + font = current_item.font() + font.setItalic(True) + font.setBold(False) + current_item.setFont(font) + current_item.setBackground(QBrush()) # 清除背景色 + else: + # 改为未隐藏样式 + current_item.setForeground(QBrush()) # 恢复默认前景色 + font = current_item.font() + font.setItalic(False) + font.setBold(True) + current_item.setFont(font) + current_item.setBackground(QBrush(QColor("#e8f5e9"))) # 浅绿色背景 + + def on_field_selected(self, current, previous): + # 保存前一个字段的更改 + if previous is not None and not self.is_separator_item(previous): + previous_field_name = previous.data(Qt.ItemDataRole.UserRole) + self.save_current_field_changes(previous_field_name) + + if not current or self.is_separator_item(current): + # 清空右侧面板 + self.name_label.setText("") + self.display_name_edit.clear() + self.category_combo.setCurrentText("") + self.type_combo.setCurrentText("auto") + self.decimal_spinbox.setValue(2) + self.hidden_checkbox.setChecked(False) + self.tooltip_edit.clear() + self.min_edit.clear() + self.max_edit.clear() + self.regex_edit.clear() + self.required_checkbox.setChecked(False) + return + + field_name = current.data(Qt.ItemDataRole.UserRole) + display_name = current.data(Qt.ItemDataRole.UserRole + 1) + 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) + # 从规则中获取显示名称,如果规则中没有则使用当前存储的显示名称 + rule_display_name = self.rules.get("display_names", {}).get(field_name, display_name) + self.display_name_edit.setText(rule_display_name) # 查找配置项所属的分组 category = "未分类" @@ -523,8 +714,8 @@ class ConfigManagementWindow(QDialog): def accept(self): """重写accept方法,确保保存当前字段的更改""" current_item = self.fields_list.currentItem() - if current_item: - field_name = current_item.text() + if current_item and not self.is_separator_item(current_item): + field_name = current_item.data(Qt.ItemDataRole.UserRole) self.save_current_field_changes(field_name) super().accept() @@ -571,10 +762,8 @@ class ConfigManagementWindow(QDialog): # 清空隐藏列表 self.rules["hidden"] = [] - # 更新当前选中字段的隐藏状态 - current_item = self.fields_list.currentItem() - if current_item: - self.hidden_checkbox.setChecked(False) + # 重新加载数据以更新显示 + self.load_data() QMessageBox.information(self, "完成", "已取消所有配置项的隐藏状态") @@ -590,13 +779,158 @@ class ConfigManagementWindow(QDialog): # 将所有配置项添加到隐藏列表 self.rules["hidden"] = all_fields - # 更新当前选中字段的隐藏状态 - current_item = self.fields_list.currentItem() - if current_item: - self.hidden_checkbox.setChecked(True) + # 重新加载数据以更新显示 + self.load_data() QMessageBox.information(self, "完成", "已隐藏所有配置项") + # === 新增:导入规则文件功能 === + def import_rules_file(self): + """导入规则文件""" + # 确认保存当前更改 + current_item = self.fields_list.currentItem() + if current_item and not self.is_separator_item(current_item): + field_name = current_item.data(Qt.ItemDataRole.UserRole) + self.save_current_field_changes(field_name) + + # 选择要导入的规则文件 + file_path, _ = QFileDialog.getOpenFileName( + self, "选择规则文件", + str(Path.home()), + "JSON规则文件 (*.json);;所有文件 (*)" + ) + + if not file_path: + return + + try: + # 读取规则文件 + with open(file_path, 'r', encoding='utf-8') as f: + imported_rules = json.load(f) + + # 验证规则文件格式 + if not isinstance(imported_rules, dict): + QMessageBox.warning(self, "导入失败", "规则文件格式不正确") + return + + # 显示导入确认对话框 + reply = QMessageBox.question( + self, "确认导入", + f"确定要导入规则文件吗?\n\n文件: {os.path.basename(file_path)}\n\n" + f"这将覆盖当前的规则设置。", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply != QMessageBox.StandardButton.Yes: + return + + # 完全替换现有规则 + self.rules.clear() + self.rules.update(imported_rules) + + # 确保规则结构完整 + self.ensure_rule_structure() + + # 重新加载数据 + self.load_data() + + QMessageBox.information( + self, "导入成功", + f"规则文件导入成功!\n\n" + f"已导入 {len(self.rules.get('display_names', {}))} 个显示名称设置\n" + f"已导入 {len(self.rules.get('categories', {}))} 个分组设置\n" + f"已导入 {len(self.rules.get('hidden', []))} 个隐藏设置" + ) + + except json.JSONDecodeError: + QMessageBox.warning(self, "导入失败", "规则文件格式不正确,不是有效的JSON文件") + except Exception as e: + QMessageBox.critical(self, "导入失败", f"导入规则文件时发生错误:{str(e)}") + + # === 新增:导出规则文件功能 === + def export_rules_file(self): + """导出规则文件""" + # 确认保存当前更改 + current_item = self.fields_list.currentItem() + if current_item and not self.is_separator_item(current_item): + field_name = current_item.data(Qt.ItemDataRole.UserRole) + self.save_current_field_changes(field_name) + + # 选择导出位置和文件名 + default_name = f"config_editor_rules_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + file_path, _ = QFileDialog.getSaveFileName( + self, "导出规则文件", + default_name, + "JSON规则文件 (*.json);;所有文件 (*)" + ) + + if not file_path: + return + + # 确保文件扩展名是.json + if not file_path.lower().endswith('.json'): + file_path += '.json' + + try: + # 准备导出的规则数据 + export_data = self.rules.copy() + + # 添加导出信息 + export_data["_export_info"] = { + "export_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "export_version": "1.0", + "note": "配置文件编辑器规则文件" + } + + # 添加统计信息 + export_data["_statistics"] = { + "total_fields": len(self.all_fields), + "display_names_count": len(self.rules.get("display_names", {})), + "categories_count": len(self.rules.get("categories", {})), + "hidden_fields_count": len(self.rules.get("hidden", [])), + "field_types_count": len(self.rules.get("field_types", {})), + "validations_count": len(self.rules.get("validations", {})) + } + + # 写入文件 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(export_data, f, indent=2, ensure_ascii=False) + + QMessageBox.information( + self, "导出成功", + f"规则文件已成功导出到:\n{file_path}\n\n" + f"导出时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + f"规则数量:{len(export_data) - 2}" # 减去_export_info和_statistics + ) + + except Exception as e: + QMessageBox.critical(self, "导出失败", f"导出规则文件时发生错误:{str(e)}") + + def ensure_rule_structure(self): + """确保规则结构完整""" + # 确保规则中有必要的字段 + if "categories" not in self.rules: + self.rules["categories"] = {"未分类": []} + if "未分类" not in self.rules["categories"]: + self.rules["categories"]["未分类"] = [] + if "display_names" not in self.rules: + self.rules["display_names"] = {} + if "field_types" not in self.rules: + self.rules["field_types"] = {} + if "field_decimals" not in self.rules: + self.rules["field_decimals"] = {} + if "tooltips" not in self.rules: + self.rules["tooltips"] = {} + if "hidden" not in self.rules: + self.rules["hidden"] = [] + if "validations" not in self.rules: + self.rules["validations"] = {} + if "field_order" not in self.rules: + self.rules["field_order"] = {} + if "last_saved_values" not in self.rules: + self.rules["last_saved_values"] = {} + def get_updated_rules(self): """获取更新后的规则""" return self.rules @@ -619,7 +953,8 @@ 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): + line_number=None, original_lines=None, validation=None, + last_saved_value=None): # 新增:上次保存的值 self.name = name self.value = value self.category = category @@ -631,6 +966,7 @@ class ConfigField: self.line_number = line_number # 记录配置项在文件中的行号 self.original_lines = original_lines or [] # 保存原始行内容(包括注释) self.validation = validation or {} # 校验规则 + self.last_saved_value = last_saved_value # 新增:上次保存的值 def get_actual_field_type(self): """获取实际的字段类型""" @@ -731,6 +1067,12 @@ class ConfigEditor(QMainWindow): self.modified_fields = set() # 记录被修改的字段 self.field_containers = {} # 存储字段的容器 + # === 新增:拖拽相关属性 === + self.dragging_field = None # 当前正在拖拽的字段名 + self.drag_start_pos = None # 拖拽起始位置 + self.drop_target_field = None # 拖拽目标字段名 + self.field_order = {} # 存储每个分组中字段的顺序,格式:{category: [field1, field2, ...]} + self.init_ui() self.load_rules() @@ -742,6 +1084,70 @@ class ConfigEditor(QMainWindow): # 非首次启动,加载配置文件 self.load_config() + def load_icon(self): + """加载程序图标 - 创建一个简单的CE色块图标""" + from PyQt6.QtGui import QIcon, QPixmap, QPainter, QFont + from PyQt6.QtCore import Qt + import os + + # 方法1: 尝试加载本地图标文件 + icon_paths = [ + os.path.join(self.program_dir, "logo.ico"), + os.path.join(self.program_dir, "logo.png"), + os.path.join(self.program_dir, "resources", "logo.ico"), + os.path.join(self.program_dir, "resources", "logo.png"), + ] + + for path in icon_paths: + if os.path.exists(path): + try: + icon = QIcon(path) + if not icon.isNull(): + print(f"加载图标: {path}") + return icon + except Exception as e: + print(f"加载图标失败 {path}: {e}") + + # 方法2: 创建简单的CE色块图标(无需外部文件) + try: + # 创建不同尺寸的图标以确保在各种场景下都能清晰显示 + sizes = [16, 32, 48, 64, 128] + icon = QIcon() + + for size in sizes: + pixmap = QPixmap(size, size) + pixmap.fill(Qt.GlobalColor.transparent) + + # 在pixmap上绘制 + from PyQt6.QtGui import QPen, QBrush, QColor + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # 绘制蓝色圆角矩形背景 + bg_color = QColor(33, 150, 243) # 蓝色 + painter.setBrush(QBrush(bg_color)) + painter.setPen(Qt.PenStyle.NoPen) + + # 根据尺寸调整圆角 + corner_radius = size // 8 + painter.drawRoundedRect(0, 0, size, size, corner_radius, corner_radius) + + # 绘制白色CE文字 + painter.setPen(QPen(Qt.GlobalColor.white, 1)) + font_size = max(size // 3, 8) # 确保字体不会太小 + painter.setFont(QFont("Arial", font_size, QFont.Weight.Bold)) + painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "CE") + + painter.end() + + # 将pixmap添加到图标 + icon.addPixmap(pixmap) + + return icon + except Exception as e: + print(f"创建图标失败: {e}") + return QIcon() # 返回空图标 + def load_user_settings(self): """加载用户设置""" try: @@ -771,6 +1177,9 @@ class ConfigEditor(QMainWindow): self.setGeometry(100, 100, 1000, 700) self.resize(1000, 700) + # 设置窗口图标(显示在标题栏和任务栏) + self.setWindowIcon(self.load_icon()) + # 创建菜单栏 menubar = self.menuBar() @@ -809,6 +1218,16 @@ class ConfigEditor(QMainWindow): self.rules_action.triggered.connect(self.manage_rules) tools_menu.addAction(self.rules_action) + # === 新增:在工具菜单中添加导入导出规则文件 === + tools_menu.addSeparator() + self.import_rules_action = QAction("导入规则文件", self) + self.import_rules_action.triggered.connect(self.import_rules_file) + tools_menu.addAction(self.import_rules_action) + + self.export_rules_action = QAction("导出规则文件", self) + self.export_rules_action.triggered.connect(self.export_rules_file) + tools_menu.addAction(self.export_rules_action) + # 状态栏 self.statusBar().showMessage("就绪") @@ -819,24 +1238,30 @@ class ConfigEditor(QMainWindow): 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; + background-color: #f8f9fa; + border: 1px solid #dee2e6; border-radius: 4px; } """) info_layout = QHBoxLayout(info_frame) - info_layout.setContentsMargins(8, 4, 8, 4) - + info_layout.setContentsMargins(12, 8, 12, 8) + self.path_label = QLabel() - self.path_label.setStyleSheet("color: #666666; font-size: 11px;") + self.path_label.setStyleSheet(""" + QLabel { + color: #495057; + font-size: 11px; + font-family: 'Consolas', 'Monaco', monospace; + } + """) info_layout.addWidget(self.path_label) info_layout.addStretch() - + main_layout.addWidget(info_frame) self.tab_widget = QTabWidget() @@ -973,12 +1398,45 @@ class ConfigEditor(QMainWindow): def init_category_tabs(self): """初始化分类标签页""" + # 清空现有的标签页 + self.tab_widget.clear() self.category_widgets.clear() + + # 获取所有分组 categories = list(self.rules.get("categories", {}).keys()) if not categories: categories = ["未分类"] + # 统计每个分组中未隐藏的配置项数量 + category_counts = {} for category in categories: + # 获取该分组中所有配置项 + category_fields = self.rules.get("categories", {}).get(category, []) + # 统计未隐藏的配置项数量 + visible_count = 0 + for field_name in category_fields: + if field_name in self.config_fields: # config_fields只包含未隐藏的配置项 + visible_count += 1 + category_counts[category] = visible_count + + # 检查是否需要隐藏"未分类"分组 + hide_uncategorized = False + if "未分类" in categories: + unclassified_count = category_counts.get("未分类", 0) + # 如果有其他分组存在 + other_categories = [c for c in categories if c != "未分类"] + has_other_categories = len(other_categories) > 0 + + # 如果"未分类"分组没有未隐藏的配置项,且存在其他分组,则隐藏 + if unclassified_count == 0 and has_other_categories: + hide_uncategorized = True + + # 创建标签页 + for category in categories: + # 如果是"未分类"分组且需要隐藏,则跳过 + if category == "未分类" and hide_uncategorized: + continue + self.create_category_tab(category) def create_category_tab(self, category): @@ -1004,47 +1462,167 @@ class ConfigEditor(QMainWindow): self.tab_widget.addTab(tab, category) def load_rules(self): - """加载规则文件""" + """加载规则文件,如果已有规则文件则保留,否则创建默认""" try: + # 先备份现有规则文件 + self.backup_existing_rules() + + # 检查是否已有规则文件 if os.path.exists(self.rules_file): with open(self.rules_file, 'r', encoding='utf-8') as f: - self.rules = json.load(f) - - # 确保规则中有"未分类"分组 - if "categories" not in self.rules: - self.rules["categories"] = {} - if "未分类" not in self.rules["categories"]: - self.rules["categories"]["未分类"] = [] - - # 确保规则中有field_decimals字段 - if "field_decimals" not in self.rules: - self.rules["field_decimals"] = {} + existing_rules = json.load(f) + + # 检查规则文件是否为空或格式错误 + if not existing_rules or not isinstance(existing_rules, dict): + raise ValueError("规则文件为空或格式错误") + + # 从打包资源加载默认规则(用于合并新字段) + default_rules = self.get_default_rules() + + # 智能合并:保留现有规则,只添加新字段 + self.rules = self.merge_rules(existing_rules, default_rules) + + # 保存合并后的规则(包含新字段) + self.save_rules() + else: - self.rules = { - "categories": {"未分类": []}, # 保留"未分类"分组 - "display_names": {}, - "tooltips": {}, - "field_types": {}, - "field_decimals": {}, # 新增:小数位数设置 - "hidden": [], # 新增隐藏列表 - "validations": {} # 新增校验规则 - } - # 首次运行,尝试创建规则文件 + # 如果没有规则文件,使用默认规则 + self.rules = self.get_default_rules() + # 首次运行,创建规则文件 try: self.save_rules() except Exception as e: print(f"创建规则文件失败: {e}") + + except (json.JSONDecodeError, ValueError, KeyError) as e: + # 如果规则文件损坏,备份后使用默认规则 + print(f"加载规则文件失败: {e},将使用默认规则") + self.backup_corrupted_rules() + self.rules = self.get_default_rules() + self.save_rules() + except Exception as e: print(f"加载规则文件失败: {e}") - self.rules = { - "categories": {"未分类": []}, - "display_names": {}, - "tooltips": {}, - "field_types": {}, - "field_decimals": {}, - "hidden": [], - "validations": {} - } + self.rules = self.get_default_rules() + + # 确保规则中有必要的字段 + self.ensure_rule_structure() + + def get_default_rules(self): + """获取默认规则""" + return { + "categories": {"未分类": []}, + "display_names": {}, + "tooltips": {}, + "field_types": {}, + "field_decimals": {}, + "hidden": [], + "validations": {}, + "field_order": {}, + "last_saved_values": {} # 新增:上次保存的值 + } + + def merge_rules(self, existing_rules, default_rules): + """智能合并规则:保留现有规则,只添加默认规则中的新字段""" + merged_rules = existing_rules.copy() + + # 确保所有必需的字段都存在 + for key in default_rules: + if key not in merged_rules: + merged_rules[key] = default_rules[key] + + # 确保"未分类"分组始终存在 + if "categories" not in merged_rules: + merged_rules["categories"] = {} + if "未分类" not in merged_rules["categories"]: + merged_rules["categories"]["未分类"] = [] + + # 确保其他必要字段存在 + for field in ["display_names", "tooltips", "field_types", "field_decimals", + "hidden", "validations", "field_order", "last_saved_values"]: # 新增last_saved_values + if field not in merged_rules: + if field == "hidden": + merged_rules[field] = [] + else: + merged_rules[field] = {} + + return merged_rules + + def ensure_rule_structure(self): + """确保规则结构完整""" + # 确保规则中有"未分类"分组 + if "categories" not in self.rules: + self.rules["categories"] = {} + if "未分类" not in self.rules["categories"]: + self.rules["categories"]["未分类"] = [] + + # 确保规则中有field_decimals字段 + if "field_decimals" not in self.rules: + self.rules["field_decimals"] = {} + + # 确保规则中有field_order字段 + if "field_order" not in self.rules: + self.rules["field_order"] = {} + + # 确保规则中有last_saved_values字段 + if "last_saved_values" not in self.rules: + self.rules["last_saved_values"] = {} + + def backup_existing_rules(self): + """备份现有规则""" + if os.path.exists(self.rules_file): + try: + # 创建备份目录 + backup_dir = os.path.join(self.program_dir, "backups") + os.makedirs(backup_dir, exist_ok=True) + + # 生成带时间戳的备份文件名 + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = os.path.join(backup_dir, f"config_editor_rules_{timestamp}.json") + + # 备份文件 + shutil.copy2(self.rules_file, backup_file) + print(f"已备份规则文件到: {backup_file}") + + # 清理旧的备份文件(保留最近5个) + self.cleanup_old_backups(backup_dir) + + except Exception as e: + print(f"备份规则文件失败: {e}") + + def cleanup_old_backups(self, backup_dir): + """清理旧的备份文件(保留最近5个)""" + try: + # 获取所有备份文件 + backup_files = [] + for filename in os.listdir(backup_dir): + if filename.startswith("config_editor_rules_") and filename.endswith(".json"): + filepath = os.path.join(backup_dir, filename) + backup_files.append((filepath, os.path.getmtime(filepath))) + + # 按修改时间排序(从旧到新) + backup_files.sort(key=lambda x: x[1]) + + # 删除旧的备份文件,保留最近5个 + if len(backup_files) > 5: + files_to_delete = backup_files[:-5] # 删除除最近5个外的所有文件 + for filepath, _ in files_to_delete: + os.remove(filepath) + print(f"已删除旧备份文件: {filepath}") + + except Exception as e: + print(f"清理旧备份文件失败: {e}") + + def backup_corrupted_rules(self): + """备份损坏的规则文件""" + if os.path.exists(self.rules_file): + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = f"{self.rules_file}.corrupted_{timestamp}.bak" + try: + shutil.copy2(self.rules_file, backup_file) + print(f"已备份损坏的规则文件到: {backup_file}") + except Exception as e: + print(f"备份损坏规则文件失败: {e}") def save_rules(self): """保存规则文件""" @@ -1054,6 +1632,112 @@ class ConfigEditor(QMainWindow): except Exception as e: print(f"保存规则文件失败: {e}") + # === 新增:在主界面直接导入导出规则文件 === + def import_rules_file(self): + """在主界面直接导入规则文件""" + # 检查当前是否有未保存的规则更改 + if hasattr(self, 'rules_modified') and self.rules_modified: + reply = QMessageBox.question( + self, "确认导入", + "当前有未保存的规则更改,导入将覆盖这些更改。\n是否继续?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + if reply != QMessageBox.StandardButton.Yes: + return + + # 选择要导入的规则文件 + file_path, _ = QFileDialog.getOpenFileName( + self, "选择规则文件", + str(Path.home()), + "JSON规则文件 (*.json);;所有文件 (*)" + ) + + if not file_path: + return + + try: + # 读取规则文件 + with open(file_path, 'r', encoding='utf-8') as f: + imported_rules = json.load(f) + + # 验证规则文件格式 + if not isinstance(imported_rules, dict): + QMessageBox.warning(self, "导入失败", "规则文件格式不正确") + return + + # 显示导入确认对话框 + reply = QMessageBox.question( + self, "确认导入", + f"确定要导入规则文件吗?\n\n文件: {os.path.basename(file_path)}\n\n" + f"这将覆盖当前的规则设置,并重新加载配置文件。", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply != QMessageBox.StandardButton.Yes: + return + + # 完全替换现有规则 + self.rules.clear() + self.rules.update(imported_rules) + + # 确保规则结构完整 + self.ensure_rule_structure() + + # 保存到规则文件 + self.save_rules() + + # 重新加载配置(这会应用新规则) + self.load_config() + + QMessageBox.information( + self, "导入成功", + f"规则文件导入成功!\n\n" + f"已导入 {len(self.rules.get('display_names', {}))} 个显示名称设置\n" + f"已导入 {len(self.rules.get('categories', {}))} 个分组设置\n" + f"配置文件已重新加载以应用新规则。" + ) + + except json.JSONDecodeError: + QMessageBox.warning(self, "导入失败", "规则文件格式不正确,不是有效的JSON文件") + except Exception as e: + QMessageBox.critical(self, "导入失败", f"导入规则文件时发生错误:{str(e)}") + + def export_rules_file(self): + """在主界面直接导出规则文件""" + # 首先保存当前规则到文件(确保最新) + self.save_rules() + + # 选择导出位置和文件名 + default_name = f"config_editor_rules_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + file_path, _ = QFileDialog.getSaveFileName( + self, "导出规则文件", + default_name, + "JSON规则文件 (*.json);;所有文件 (*)" + ) + + if not file_path: + return + + # 确保文件扩展名是.json + if not file_path.lower().endswith('.json'): + file_path += '.json' + + try: + # 复制规则文件到指定位置 + shutil.copy2(self.rules_file, file_path) + + QMessageBox.information( + self, "导出成功", + f"规则文件已成功导出到:\n{file_path}\n\n" + f"导出时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + f"规则文件:{os.path.basename(self.rules_file)}" + ) + + except Exception as e: + QMessageBox.critical(self, "导出失败", f"导出规则文件时发生错误:{str(e)}") + def categorize_field(self, field_name): """为字段分类,如果未分配分组则归到'未分类'""" for category, fields in self.rules.get("categories", {}).items(): @@ -1097,6 +1781,13 @@ class ConfigEditor(QMainWindow): """检查配置项是否被标记为隐藏""" return field_name in self.rules.get("hidden", []) + def get_last_saved_value(self, field_name, current_value): + """获取配置项的上次保存值,如果没有则使用当前值""" + last_saved_values = self.rules.get("last_saved_values", {}) + if field_name in last_saved_values: + return last_saved_values[field_name] + return None # 第一次没有上次保存值,返回None + def parse_config_file(self): """解析配置文件,精确匹配每个配置项的位置和注释""" try: @@ -1160,6 +1851,9 @@ class ConfigEditor(QMainWindow): field_type = self.get_field_type(var_name, var_value) decimals = self.get_field_decimals(var_name) if field_type == "float" else None + # 获取上次保存的值 + last_saved_value = self.get_last_saved_value(var_name, var_value) + all_config_fields[var_name] = ConfigField( name=var_name, value=var_value, @@ -1171,7 +1865,8 @@ class ConfigEditor(QMainWindow): hidden=self.is_hidden(var_name), line_number=assignment_line - len(comment_lines), original_lines=full_block, - validation=self.get_validation(var_name) + validation=self.get_validation(var_name), + last_saved_value=last_saved_value # 新增:设置上次保存的值 ) except: @@ -1180,6 +1875,9 @@ class ConfigEditor(QMainWindow): value_str = ast.get_source_segment(content, node.value) config_data[var_name] = value_str + # 获取上次保存的值 + last_saved_value = self.get_last_saved_value(var_name, value_str) + all_config_fields[var_name] = ConfigField( name=var_name, value=value_str, @@ -1191,11 +1889,16 @@ class ConfigEditor(QMainWindow): hidden=self.is_hidden(var_name), line_number=assignment_line - len(comment_lines), original_lines=full_block, - validation=self.get_validation(var_name) + validation=self.get_validation(var_name), + last_saved_value=last_saved_value # 新增:设置上次保存的值 ) except: config_data[var_name] = "无法解析的值" + + # 获取上次保存的值 + last_saved_value = self.get_last_saved_value(var_name, "无法解析的值") + all_config_fields[var_name] = ConfigField( name=var_name, value="无法解析的值", @@ -1207,7 +1910,8 @@ class ConfigEditor(QMainWindow): hidden=self.is_hidden(var_name), line_number=assignment_line - len(comment_lines), original_lines=full_block, - validation=self.get_validation(var_name) + validation=self.get_validation(var_name), + last_saved_value=last_saved_value # 新增:设置上次保存的值 ) return config_data, all_config_fields, content @@ -1304,10 +2008,10 @@ class ConfigEditor(QMainWindow): container = self.field_containers[field_name] # 只修改背景色,使用更精确的选择器确保不影响子控件 container.setStyleSheet(""" - QFrame#field_container { + QFrame#field_container_%s { background-color: #fff9c4; } - """) + """ % field_name) def clear_field_highlight(self, field_name): """清除字段的高亮显示,恢复原有样式""" @@ -1315,10 +2019,17 @@ class ConfigEditor(QMainWindow): container = self.field_containers[field_name] # 恢复原始样式,不影响子控件 container.setStyleSheet(""" - QFrame#field_container { + QFrame#field_container_%s { + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 4px; background-color: transparent; } - """) + QFrame#field_container_%s:hover { + border: 1px solid #2196F3; + background-color: #f8fdff; + } + """ % (field_name, field_name)) def update_status_bar(self): """更新状态栏显示修改数量""" @@ -1327,30 +2038,277 @@ class ConfigEditor(QMainWindow): else: self.statusBar().showMessage("就绪") + # === 新增:鼠标按下事件处理器 === + def create_mouse_press_handler(self, field_name, label_widget): + """创建鼠标按下事件处理器""" + def mouse_press_handler(event): + if event.button() == Qt.MouseButton.LeftButton: + self.dragging_field = field_name + self.drag_start_pos = event.pos() + + # 更改光标为抓取手型 + label_widget.setCursor(Qt.CursorShape.ClosedHandCursor) + + # 高亮当前拖拽的字段 + self.highlight_dragging_field(field_name, True) + + # 开始拖拽操作 + drag = QDrag(label_widget) + mime_data = QMimeData() + mime_data.setText(field_name) + drag.setMimeData(mime_data) + + # 设置拖拽时的缩略图 + pixmap = label_widget.grab() + drag.setPixmap(pixmap) + drag.setHotSpot(event.pos()) + + drag.exec(Qt.DropAction.MoveAction) + + # 拖拽结束后恢复 + label_widget.setCursor(Qt.CursorShape.OpenHandCursor) + self.highlight_dragging_field(field_name, False) + self.dragging_field = None + return mouse_press_handler + + # === 新增:事件过滤器,用于处理拖拽进入、离开和放置 === + def eventFilter(self, obj, event): + """事件过滤器,用于处理拖拽相关事件""" + # 检查是否是字段容器 + if isinstance(obj, QFrame) and obj.objectName().startswith("field_container_"): + if event.type() == QEvent.Type.DragEnter: + # 拖拽进入容器 + self.handle_drag_enter_event(obj, event) + return True + elif event.type() == QEvent.Type.DragLeave: + # 拖拽离开容器 + self.handle_drag_leave_event(obj) + return True + elif event.type() == QEvent.Type.Drop: + # 放置到容器 + self.handle_drop_event(obj, event) + return True + elif event.type() == QEvent.Type.DragMove: + # 拖拽在容器上移动 + self.handle_drag_move_event(obj, event) + return True + + return super().eventFilter(obj, event) + + # === 新增:处理拖拽进入事件 === + def handle_drag_enter_event(self, container, event): + """处理拖拽进入事件""" + # 获取容器对应的字段名 + field_name = container.objectName().replace("field_container_", "") + + # 如果不是拖拽自身,则接受拖拽 + if field_name != self.dragging_field: + event.acceptProposedAction() + self.drop_target_field = field_name + self.highlight_drop_target(field_name, True) + + # === 新增:处理拖拽离开事件 === + def handle_drag_leave_event(self, container): + """处理拖拽离开事件""" + # 获取容器对应的字段名 + field_name = container.objectName().replace("field_container_", "") + + # 清除目标高亮 + if field_name == self.drop_target_field: + self.highlight_drop_target(field_name, False) + self.drop_target_field = None + + # === 新增:处理放置事件 === + def handle_drop_event(self, container, event): + """处理放置事件""" + # 获取源字段和目标字段 + source_field = self.dragging_field + target_field = container.objectName().replace("field_container_", "") + + if source_field and target_field and source_field != target_field: + # 获取字段的分组 + source_config = self.all_config_fields.get(source_field) + target_config = self.all_config_fields.get(target_field) + + if source_config and target_config and source_config.category == target_config.category: + # 在同一分组内移动字段 + self.reorder_fields_in_category(source_field, target_field, source_config.category) + + # 重新生成UI + show_hidden = getattr(self, 'show_hidden', False) + self.generate_dynamic_ui(show_hidden) + + # 保存顺序到规则文件 + self.save_field_order_to_rules() + + # === 修复:不显示弹窗,只更新状态栏 === + self.statusBar().showMessage(f"已将 '{source_config.display_name}' 移动到 '{target_config.display_name}' 的位置", 3000) + + # 清除目标高亮 + self.highlight_drop_target(target_field, False) + self.drop_target_field = None + event.acceptProposedAction() + + # === 新增:处理拖拽移动事件 === + def handle_drag_move_event(self, container, event): + """处理拖拽移动事件""" + # 获取容器对应的字段名 + field_name = container.objectName().replace("field_container_", "") + + # 如果不是拖拽自身,则接受拖拽移动 + if field_name != self.dragging_field: + event.acceptProposedAction() + + # === 新增:高亮显示拖拽中的字段 === + def highlight_dragging_field(self, field_name, is_dragging): + """高亮显示拖拽中的字段""" + if field_name in self.field_containers: + container = self.field_containers[field_name] + if is_dragging: + container.setStyleSheet(f""" + QFrame#field_container_{field_name} {{ + border: 2px dashed #2196F3; + background-color: #e3f2fd; + opacity: 0.7; + }} + """) + else: + container.setStyleSheet(f""" + QFrame#field_container_{field_name} {{ + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 4px; + background-color: transparent; + }} + QFrame#field_container_{field_name}:hover {{ + border: 1px solid #2196F3; + background-color: #f8fdff; + }} + """) + + # === 新增:高亮显示放置目标 === + def highlight_drop_target(self, field_name, is_target): + """高亮显示放置目标""" + if field_name in self.field_containers: + container = self.field_containers[field_name] + if is_target: + container.setStyleSheet(f""" + QFrame#field_container_{field_name} {{ + border: 2px solid #4CAF50; + background-color: #e8f5e9; + }} + """) + else: + container.setStyleSheet(f""" + QFrame#field_container_{field_name} {{ + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 4px; + background-color: transparent; + }} + QFrame#field_container_{field_name}:hover {{ + border: 1px solid #2196F3; + background-color: #f8fdff; + }} + """) + + # === 新增:重新排序分组内的字段 === + def reorder_fields_in_category(self, source_field, target_field, category): + """重新排序分组内的字段""" + # 获取当前分组内的字段顺序 + if category not in self.field_order: + # 如果还没有该分组的顺序记录,从配置字段中获取 + fields_in_category = [] + for field_name, config_field in self.all_config_fields.items(): + if config_field.category == category: + fields_in_category.append(field_name) + self.field_order[category] = fields_in_category + + # 重新排序 + order_list = self.field_order[category] + + # 移除源字段 + if source_field in order_list: + order_list.remove(source_field) + + # 找到目标字段的位置,在目标位置插入源字段 + if target_field in order_list: + target_index = order_list.index(target_field) + order_list.insert(target_index, source_field) + else: + # 如果目标字段不在列表中(不应该发生),添加到末尾 + order_list.append(source_field) + + # === 新增:保存字段顺序到规则文件 === + def save_field_order_to_rules(self): + """保存字段顺序到规则文件""" + # 将字段顺序转换为规则格式 + self.rules["field_order"] = self.field_order + + # 保存规则文件 + 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 initialize_field_order(self): + """初始化字段顺序""" + # 从规则中加载字段顺序 + if "field_order" in self.rules: + self.field_order = self.rules["field_order"] + else: + self.field_order = {} + + # 确保所有字段都在顺序列表中 + for field_name, config_field in self.all_config_fields.items(): + category = config_field.category + if category not in self.field_order: + self.field_order[category] = [] + + if field_name not in self.field_order[category]: + self.field_order[category].append(field_name) + def create_field_editor(self, config_field): - """创建字段编辑器,返回容器和控件""" + """创建字段编辑器,返回容器和控件(新增拖拽支持)""" # 创建容器框架 container = QFrame() - container.setObjectName("field_container") # 设置对象名用于样式选择 + container.setObjectName(f"field_container_{config_field.name}") # 设置唯一ID container.setFrameShape(QFrame.Shape.StyledPanel) - # 设置基础样式,使用对象名选择器确保只影响容器本身 + + # === 新增:设置容器可接受拖拽事件 === + container.setAcceptDrops(True) + container.installEventFilter(self) + + # 设置基础样式 container.setStyleSheet(""" - QFrame#field_container { + QFrame#field_container_%s { border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; background-color: transparent; } - """) + QFrame#field_container_%s:hover { + border: 1px solid #2196F3; + background-color: #f8fdff; + } + """ % (config_field.name, config_field.name)) layout = QHBoxLayout(container) layout.setContentsMargins(8, 4, 8, 4) layout.setSpacing(8) - # 标签 + # 标签 - 设置为可拖拽的区域 label = QLabel(config_field.display_name) label.setMinimumWidth(180) + # === 新增:为标签添加拖拽支持 === + label.setProperty("field_name", config_field.name) # 存储字段名 + label.setProperty("category", config_field.category) # 存储分组名 + label.mousePressEvent = self.create_mouse_press_handler(config_field.name, label) + label.setCursor(Qt.CursorShape.OpenHandCursor) # 设置手型光标 + # 添加校验指示器(如果配置项有校验规则) validation_indicator = "" if config_field.validation: @@ -1387,6 +2345,38 @@ class ConfigEditor(QMainWindow): name_label.setToolTip(f"配置变量名: {config_field.name}") layout.addWidget(name_label) + # === 新增:上次保存的值显示 === + last_saved_label = QLabel() + last_saved_label.setMinimumWidth(120) + last_saved_label.setToolTip("上次保存的值") + + # 格式化显示上次保存的值 + if config_field.last_saved_value is not None: + # 检查是否是复杂类型 + if isinstance(config_field.last_saved_value, (dict, list)): + # 对于复杂类型,使用JSON格式化 + try: + formatted = json.dumps(config_field.last_saved_value, ensure_ascii=False) + # 如果太长,截断显示 + if len(formatted) > 20: + formatted = formatted[:17] + "..." + last_saved_label.setText(f"上次: {formatted}") + except: + value_type = type(config_field.last_saved_value).__name__ + last_saved_label.setText(f"上次: [{value_type}]") + else: + # 对于简单类型,显示完整值 + value_str = str(config_field.last_saved_value) + # 如果太长,截断显示 + if len(value_str) > 20: + value_str = value_str[:17] + "..." + last_saved_label.setText(f"上次: {value_str}") + else: + last_saved_label.setText("上次: 无") + + last_saved_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic; background-color: #f8f8f8; padding: 2px; border-radius: 2px;") + layout.addWidget(last_saved_label) + layout.addStretch() # 存储容器引用 @@ -1395,10 +2385,13 @@ class ConfigEditor(QMainWindow): return container, widget def generate_dynamic_ui(self, show_hidden=False): - """生成动态UI,可选择是否显示隐藏的配置项""" + """生成动态UI,可选择是否显示隐藏的配置项,支持自定义顺序""" self.dynamic_widgets.clear() - self.modified_fields.clear() # 重置修改记录 - self.field_containers.clear() # 清空容器引用 + self.modified_fields.clear() + self.field_containers.clear() + + # 重新初始化标签页(考虑是否隐藏"未分类"分组) + self.init_category_tabs() for category, layout in self.category_widgets.items(): while layout.count(): @@ -1416,18 +2409,45 @@ class ConfigEditor(QMainWindow): categorized_fields[category] = [] categorized_fields[category].append(config_field) + # 初始化字段顺序 + self.initialize_field_order() + for category, fields in categorized_fields.items(): + # 跳过不在当前标签页中的分组(如被隐藏的"未分类"分组) if category not in self.category_widgets: - self.create_category_tab(category) + continue layout = self.category_widgets[category] - fields.sort(key=lambda x: x.name) + + # === 修改:按照保存的顺序排序 === + if category in self.field_order and self.field_order[category]: + # 创建从字段名到配置项的映射 + field_dict = {f.name: f for f in fields} + + # 按照保存的顺序排序 + ordered_fields = [] + for field_name in self.field_order[category]: + if field_name in field_dict: + ordered_fields.append(field_dict[field_name]) + del field_dict[field_name] + + # 添加不在顺序列表中的字段(新字段) + remaining_fields = list(field_dict.values()) + remaining_fields.sort(key=lambda x: x.name) # 按字母顺序排序剩余字段 + ordered_fields.extend(remaining_fields) + + fields = ordered_fields + else: + # 如果没有保存的顺序,按字母顺序排序 + fields.sort(key=lambda x: x.name) # 使用GroupBox,更清晰的分组 group_box = QGroupBox(f"{category}") 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) @@ -1460,8 +2480,11 @@ class ConfigEditor(QMainWindow): show_hidden = getattr(self, 'show_hidden', False) self.generate_dynamic_ui(show_hidden) + # 统计显示的标签页数量 + visible_tabs = self.tab_widget.count() hidden_count = len(self.all_config_fields) - len(self.config_fields) - self.statusBar().showMessage(f"成功加载 {len(self.config_data)} 个配置项 ({len(self.config_fields)} 个显示, {hidden_count} 个隐藏)") + + self.statusBar().showMessage(f"成功加载 {len(self.config_data)} 个配置项 ({len(self.config_fields)} 个显示, {hidden_count} 个隐藏, {visible_tabs} 个分组)") except Exception as e: QMessageBox.critical(self, "错误", f"加载配置文件失败:{str(e)}") @@ -1693,6 +2716,14 @@ class ConfigEditor(QMainWindow): self.statusBar().showMessage("用户取消了保存操作") return + # === 新增:在保存前更新上次保存的值到规则文件 === + for field_name, (old_value, new_value) in changes.items(): + # 将修改前的值(旧值)保存为上次保存的值 + self.rules.setdefault("last_saved_values", {})[field_name] = old_value + + # 立即保存规则文件,确保上次保存的值被持久化 + self.save_rules() + # 读取原始文件内容 with open(self.config_file_path, 'r', encoding='utf-8') as f: original_content = f.read() @@ -1796,7 +2827,7 @@ class ConfigEditor(QMainWindow): self.clear_field_highlight(field_name) self.modified_fields.remove(field_name) - # 重新加载配置 + # 重新加载配置(这会重新读取规则文件中的上次保存值) self.load_config() self.statusBar().showMessage(f"配置文件保存成功,修改了 {len(changes)} 个配置项") @@ -1820,4 +2851,4 @@ def main(): sys.exit(app.exec()) if __name__ == '__main__': - main() + main() \ No newline at end of file